mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Migrate to Kotlin - MultipartStreamReader (#50519)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50519 This diff migrates the following file to Kotlin - MultipartStreamReader as part of our ongoing effort of migrating the codebase to Kotlin Changelog: [Internal] [Changed] - MultipartStreamReader to Kotlin Reviewed By: rshest Differential Revision: D72561124 fbshipit-source-id: d616bfc547ea6a773ebc1c45f97111ae7c7ec85a
This commit is contained in:
committed by
Facebook GitHub Bot
parent
890b8f7ea3
commit
d12bcaac3b
-170
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react.devsupport;
|
||||
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import okio.ByteString;
|
||||
|
||||
/** Utility class to parse the body of a response of type multipart/mixed. */
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
class MultipartStreamReader {
|
||||
// Standard line separator for HTTP.
|
||||
private static final String CRLF = "\r\n";
|
||||
|
||||
private final BufferedSource mSource;
|
||||
private final String mBoundary;
|
||||
private long mLastProgressEvent;
|
||||
|
||||
public interface ChunkListener {
|
||||
/** Invoked when a chunk of a multipart response is fully downloaded. */
|
||||
void onChunkComplete(Map<String, String> headers, Buffer body, boolean isLastChunk)
|
||||
throws IOException;
|
||||
|
||||
/** Invoked as bytes of the current chunk are read. */
|
||||
void onChunkProgress(Map<String, String> headers, long loaded, long total) throws IOException;
|
||||
}
|
||||
|
||||
public MultipartStreamReader(BufferedSource source, String boundary) {
|
||||
mSource = source;
|
||||
mBoundary = boundary;
|
||||
}
|
||||
|
||||
private Map<String, String> parseHeaders(Buffer data) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
|
||||
String text = data.readUtf8();
|
||||
String[] lines = text.split(CRLF);
|
||||
for (String line : lines) {
|
||||
int indexOfSeparator = line.indexOf(":");
|
||||
if (indexOfSeparator == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String key = line.substring(0, indexOfSeparator).trim();
|
||||
String value = line.substring(indexOfSeparator + 1).trim();
|
||||
headers.put(key, value);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private void emitChunk(Buffer chunk, boolean done, ChunkListener listener) throws IOException {
|
||||
ByteString marker = ByteString.encodeUtf8(CRLF + CRLF);
|
||||
long indexOfMarker = chunk.indexOf(marker);
|
||||
if (indexOfMarker == -1) {
|
||||
listener.onChunkComplete(Collections.emptyMap(), chunk, done);
|
||||
} else {
|
||||
Buffer headers = new Buffer();
|
||||
Buffer body = new Buffer();
|
||||
chunk.read(headers, indexOfMarker);
|
||||
chunk.skip(marker.size());
|
||||
chunk.readAll(body);
|
||||
listener.onChunkComplete(parseHeaders(headers), body, done);
|
||||
}
|
||||
}
|
||||
|
||||
private void emitProgress(
|
||||
Map<String, String> headers, long contentLength, boolean isFinal, ChunkListener listener)
|
||||
throws IOException {
|
||||
if (headers == null || listener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (currentTime - mLastProgressEvent > 16 || isFinal) {
|
||||
mLastProgressEvent = currentTime;
|
||||
long headersContentLength =
|
||||
headers.get("Content-Length") != null ? Long.parseLong(headers.get("Content-Length")) : 0;
|
||||
listener.onChunkProgress(headers, contentLength, headersContentLength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all parts of the multipart response and execute the listener for each chunk received.
|
||||
*
|
||||
* @param listener Listener invoked when chunks are received.
|
||||
* @return If the read was successful
|
||||
*/
|
||||
public boolean readAllParts(ChunkListener listener) throws IOException {
|
||||
ByteString delimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + CRLF);
|
||||
ByteString closeDelimiter = ByteString.encodeUtf8(CRLF + "--" + mBoundary + "--" + CRLF);
|
||||
ByteString headersDelimiter = ByteString.encodeUtf8(CRLF + CRLF);
|
||||
|
||||
int bufferLen = 4 * 1024;
|
||||
long chunkStart = 0;
|
||||
long bytesSeen = 0;
|
||||
Buffer content = new Buffer();
|
||||
Map<String, String> currentHeaders = null;
|
||||
long currentHeadersLength = 0;
|
||||
|
||||
while (true) {
|
||||
boolean isCloseDelimiter = false;
|
||||
|
||||
// Search only a subset of chunk that we haven't seen before + few bytes
|
||||
// to allow for the edge case when the delimiter is cut by read call.
|
||||
long searchStart = Math.max(bytesSeen - closeDelimiter.size(), chunkStart);
|
||||
long indexOfDelimiter = content.indexOf(delimiter, searchStart);
|
||||
if (indexOfDelimiter == -1) {
|
||||
isCloseDelimiter = true;
|
||||
indexOfDelimiter = content.indexOf(closeDelimiter, searchStart);
|
||||
}
|
||||
|
||||
if (indexOfDelimiter == -1) {
|
||||
bytesSeen = content.size();
|
||||
|
||||
if (currentHeaders == null) {
|
||||
long indexOfHeaders = content.indexOf(headersDelimiter, searchStart);
|
||||
if (indexOfHeaders >= 0) {
|
||||
mSource.read(content, indexOfHeaders);
|
||||
Buffer headers = new Buffer();
|
||||
content.copyTo(headers, searchStart, indexOfHeaders - searchStart);
|
||||
currentHeadersLength = headers.size() + headersDelimiter.size();
|
||||
currentHeaders = parseHeaders(headers);
|
||||
}
|
||||
} else {
|
||||
emitProgress(currentHeaders, content.size() - currentHeadersLength, false, listener);
|
||||
}
|
||||
|
||||
long bytesRead = mSource.read(content, bufferLen);
|
||||
if (bytesRead <= 0) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
long chunkEnd = indexOfDelimiter;
|
||||
long length = chunkEnd - chunkStart;
|
||||
|
||||
// Ignore preamble
|
||||
if (chunkStart > 0) {
|
||||
Buffer chunk = new Buffer();
|
||||
content.skip(chunkStart);
|
||||
content.read(chunk, length);
|
||||
// NULLSAFE_FIXME[Parameter Not Nullable]
|
||||
emitProgress(currentHeaders, chunk.size() - currentHeadersLength, true, listener);
|
||||
emitChunk(chunk, isCloseDelimiter, listener);
|
||||
currentHeaders = null;
|
||||
currentHeadersLength = 0;
|
||||
} else {
|
||||
content.skip(chunkEnd);
|
||||
}
|
||||
|
||||
if (isCloseDelimiter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bytesSeen = chunkStart = delimiter.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION_ERROR") // Conflicting okio versions
|
||||
|
||||
package com.facebook.react.devsupport
|
||||
|
||||
import java.io.IOException
|
||||
import kotlin.math.max
|
||||
import okio.Buffer
|
||||
import okio.BufferedSource
|
||||
import okio.ByteString
|
||||
|
||||
/** Utility class to parse the body of a response of type multipart/mixed. */
|
||||
internal class MultipartStreamReader(
|
||||
private val source: BufferedSource,
|
||||
private val boundary: String
|
||||
) {
|
||||
private var lastProgressEvent: Long = 0
|
||||
|
||||
interface ChunkListener {
|
||||
/** Invoked when a chunk of a multipart response is fully downloaded. */
|
||||
@Throws(IOException::class)
|
||||
fun onChunkComplete(headers: Map<String, String>, body: Buffer, isLastChunk: Boolean)
|
||||
|
||||
/** Invoked as bytes of the current chunk are read. */
|
||||
@Throws(IOException::class)
|
||||
fun onChunkProgress(headers: Map<String, String>, loaded: Long, total: Long)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all parts of the multipart response and execute the listener for each chunk received.
|
||||
*
|
||||
* @param listener Listener invoked when chunks are received.
|
||||
* @return If the read was successful
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun readAllParts(listener: ChunkListener): Boolean {
|
||||
val delimiter: ByteString = ByteString.encodeUtf8("$CRLF--$boundary$CRLF")
|
||||
val closeDelimiter: ByteString = ByteString.encodeUtf8("$CRLF--$boundary--$CRLF")
|
||||
val headersDelimiter: ByteString = ByteString.encodeUtf8(CRLF + CRLF)
|
||||
|
||||
val bufferLen = 4 * 1024
|
||||
var chunkStart: Long = 0
|
||||
var bytesSeen: Long = 0
|
||||
val content = Buffer()
|
||||
var currentHeaders: Map<String, String>? = null
|
||||
var currentHeadersLength: Long = 0
|
||||
|
||||
while (true) {
|
||||
var isCloseDelimiter = false
|
||||
|
||||
// Search only a subset of chunk that we haven't seen before + few bytes
|
||||
// to allow for the edge case when the delimiter is cut by read call.
|
||||
val searchStart =
|
||||
max((bytesSeen - closeDelimiter.size()).toDouble(), chunkStart.toDouble()).toLong()
|
||||
var indexOfDelimiter = content.indexOf(delimiter, searchStart)
|
||||
if (indexOfDelimiter == -1L) {
|
||||
isCloseDelimiter = true
|
||||
indexOfDelimiter = content.indexOf(closeDelimiter, searchStart)
|
||||
}
|
||||
|
||||
if (indexOfDelimiter == -1L) {
|
||||
bytesSeen = content.size()
|
||||
|
||||
if (currentHeaders == null) {
|
||||
val indexOfHeaders = content.indexOf(headersDelimiter, searchStart)
|
||||
if (indexOfHeaders >= 0) {
|
||||
source.read(content, indexOfHeaders)
|
||||
val headers = Buffer()
|
||||
content.copyTo(headers, searchStart, indexOfHeaders - searchStart)
|
||||
currentHeadersLength = headers.size() + headersDelimiter.size()
|
||||
currentHeaders = parseHeaders(headers)
|
||||
}
|
||||
} else {
|
||||
emitProgress(currentHeaders, content.size() - currentHeadersLength, false, listener)
|
||||
}
|
||||
|
||||
val bytesRead = source.read(content, bufferLen.toLong())
|
||||
if (bytesRead <= 0) {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
val chunkEnd = indexOfDelimiter
|
||||
val length = chunkEnd - chunkStart
|
||||
|
||||
// Ignore preamble
|
||||
if (chunkStart > 0) {
|
||||
val chunk = Buffer()
|
||||
content.skip(chunkStart)
|
||||
content.read(chunk, length)
|
||||
// NULLSAFE_FIXME[Parameter Not Nullable]
|
||||
emitProgress(currentHeaders, chunk.size() - currentHeadersLength, true, listener)
|
||||
emitChunk(chunk, isCloseDelimiter, listener)
|
||||
currentHeaders = null
|
||||
currentHeadersLength = 0
|
||||
} else {
|
||||
content.skip(chunkEnd)
|
||||
}
|
||||
if (isCloseDelimiter) {
|
||||
return true
|
||||
}
|
||||
chunkStart = delimiter.size().toLong()
|
||||
bytesSeen = chunkStart
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseHeaders(data: Buffer): Map<String, String> {
|
||||
val headers: MutableMap<String, String> = mutableMapOf()
|
||||
val text = data.readUtf8()
|
||||
val lines = text.split(CRLF.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
for (line in lines) {
|
||||
val indexOfSeparator = line.indexOf(":")
|
||||
if (indexOfSeparator == -1) {
|
||||
continue
|
||||
}
|
||||
val key = line.substring(0, indexOfSeparator).trim { it <= ' ' }
|
||||
val value = line.substring(indexOfSeparator + 1).trim { it <= ' ' }
|
||||
headers[key] = value
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun emitChunk(chunk: Buffer, done: Boolean, listener: ChunkListener) {
|
||||
val marker: ByteString = ByteString.encodeUtf8(CRLF + CRLF)
|
||||
val indexOfMarker = chunk.indexOf(marker)
|
||||
if (indexOfMarker == -1L) {
|
||||
listener.onChunkComplete(emptyMap(), chunk, done)
|
||||
} else {
|
||||
val headers = Buffer()
|
||||
val body = Buffer()
|
||||
chunk.read(headers, indexOfMarker)
|
||||
chunk.skip(marker.size().toLong())
|
||||
chunk.readAll(body)
|
||||
listener.onChunkComplete(parseHeaders(headers), body, done)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun emitProgress(
|
||||
headers: Map<String, String>?,
|
||||
contentLength: Long,
|
||||
isFinal: Boolean,
|
||||
listener: ChunkListener?
|
||||
) {
|
||||
if (listener == null || headers == null) {
|
||||
return
|
||||
}
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastProgressEvent > 16 || isFinal) {
|
||||
lastProgressEvent = currentTime
|
||||
val headersContentLength = headers.getOrDefault("Content-Length", "0").toLong()
|
||||
listener.onChunkProgress(headers, contentLength, headersContentLength)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Standard line separator for HTTP.
|
||||
private const val CRLF = "\r\n"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -37,7 +37,7 @@ class MultipartStreamReaderTest {
|
||||
super.onChunkComplete(headers, body, done)
|
||||
|
||||
assertThat(done).isTrue
|
||||
assertThat(headers!!["Content-Type"]).isEqualTo("application/json; charset=utf-8")
|
||||
assertThat(headers["Content-Type"]).isEqualTo("application/json; charset=utf-8")
|
||||
assertThat(body.readUtf8()).isEqualTo("{}")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user