Fix crash due to mishandling of UTF-8 in progressive download.

Summary:
Fixes:
```
Fatal Exception: java.lang.RuntimeException: Failed to create String from JSON
       at com.facebook.react.bridge.queue.NativeRunnable.run(NativeRunnable.java)
       at android.os.Handler.handleCallback(Handler.java:739)
       at android.os.Handler.dispatchMessage(Handler.java:95)
       at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:31)
       at android.os.Looper.loop(Looper.java:234)
       at com.facebook.react.bridge.queue.MessageQueueThreadImpl$3.run(MessageQueueThreadImpl.java:193)
       at java.lang.Thread.run(Thread.java:818)
```
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.

This should fix the root cause of this issue:
https://github.com/facebook/react-native/issues/10756
Closes https://github.com/facebook/react-native/pull/15295

Differential Revision: D6712570

Pulled By: hramos

fbshipit-source-id: f07fcf0f011c2133c8e860ceb0588a29d36d07fb
This commit is contained in:
Sergei Dryganets
2018-01-12 10:57:34 -08:00
committed by Facebook Github Bot
parent 2fe7483c36
commit 9024f56bda
4 changed files with 278 additions and 5 deletions

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.common;
import java.nio.charset.Charset;
/**
* Not all versions of Android SDK have this class in nio package.
* This is the reason to have it around.
*/
public class StandardCharsets {
/**
* Eight-bit UCS Transformation Format
*/
public static final Charset UTF_8 = Charset.forName("UTF-8");
/**
* Sixteen-bit UCS Transformation Format, byte order identified by an
* optional byte-order mark
*/
public static final Charset UTF_16 = Charset.forName("UTF-16");
/**
* Sixteen-bit UCS Transformation Format, big-endian byte order
*/
public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
/**
* Sixteen-bit UCS Transformation Format, little-endian byte order
*/
public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
}

View File

@@ -14,6 +14,7 @@ import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -29,6 +30,7 @@ import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.StandardCharsets;
import com.facebook.react.common.network.OkHttpCallUtil;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
@@ -408,20 +410,24 @@ public final class NetworkingModule extends ReactContextBaseJavaModule {
// Ignore
}
Reader reader = responseBody.charStream();
Charset charset = responseBody.contentType() == null ? StandardCharsets.UTF_8 :
responseBody.contentType().charset(StandardCharsets.UTF_8);
ProgressiveStringDecoder streamDecoder = new ProgressiveStringDecoder(charset);
InputStream inputStream = responseBody.byteStream();
try {
char[] buffer = new char[MAX_CHUNK_SIZE_BETWEEN_FLUSHES];
byte[] buffer = new byte[MAX_CHUNK_SIZE_BETWEEN_FLUSHES];
int read;
while ((read = reader.read(buffer)) != -1) {
while ((read = inputStream.read(buffer)) != -1) {
ResponseUtil.onIncrementalDataReceived(
eventEmitter,
requestId,
new String(buffer, 0, read),
streamDecoder.decodeNext(buffer, read),
totalBytesRead,
contentLength);
}
} finally {
reader.close();
inputStream.close();
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.modules.network;
import com.facebook.common.logging.FLog;
import com.facebook.react.common.ReactConstants;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
/**
* Class to decode encoded strings from byte array chunks.
* As in different encodings single character could take up to 4 characters byte array passed to
* decode could have parts of the characters which can't be correctly decoded.
*
* This class is designed in assumption that original byte stream is correctly formatted string in
* given encoding. Otherwise some parts of the data won't be decoded.
*
*/
public class ProgressiveStringDecoder {
private static final String EMPTY_STRING = "";
private final CharsetDecoder mDecoder;
private byte[] remainder = null;
/**
* @param charset expected charset of the data
*/
public ProgressiveStringDecoder(Charset charset) {
mDecoder = charset.newDecoder();
}
/**
* Parses data to String
* If there is a partial multi-byte symbol on the edge of the String it get saved to the
* reminder and added to the string on the decodeNext call.
* @param data
* @return
*/
public String decodeNext(byte[] data, int length) {
byte[] decodeData;
if (remainder != null) {
decodeData = new byte[remainder.length + length];
System.arraycopy(remainder, 0, decodeData, 0, remainder.length);
System.arraycopy(data, 0, decodeData, remainder.length, length);
length += remainder.length;
} else {
decodeData = data;
}
ByteBuffer decodeBuffer = ByteBuffer.wrap(decodeData, 0, length);
CharBuffer result = null;
boolean decoded = false;
int remainderLenght = 0;
while (!decoded && (remainderLenght < 4)) {
try {
result = mDecoder.decode(decodeBuffer);
decoded = true;
} catch (CharacterCodingException e) {
remainderLenght++;
decodeBuffer = ByteBuffer.wrap(decodeData, 0, length - remainderLenght);
}
}
boolean hasRemainder = decoded && remainderLenght > 0;
if (hasRemainder) {
remainder = new byte[remainderLenght];
System.arraycopy(decodeData, length - remainderLenght, remainder, 0, remainderLenght);
} else {
remainder = null;
}
if (!decoded) {
FLog.w(ReactConstants.TAG, "failed to decode string from byte array");
return EMPTY_STRING;
} else {
return new String(result.array(), 0, result.length());
}
}
}