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:
Nicola Corti
2025-04-08 02:59:28 -07:00
committed by Facebook GitHub Bot
parent 890b8f7ea3
commit d12bcaac3b
3 changed files with 169 additions and 171 deletions
@@ -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();
}
}
}
@@ -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"
}
}
@@ -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("{}")
}
}