diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.kt index f7d4d12d4b93..a49f2c98339e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/MultipartStreamReader.kt @@ -156,7 +156,7 @@ internal class MultipartStreamReader( val marker: ByteString = ByteString.encodeUtf8(CRLF + CRLF) val indexOfMarker = content.indexOf(marker, 0) - if (indexOfMarker == -1L || indexOfMarker >= chunkLength) { + if (indexOfMarker == -1L || indexOfMarker > chunkLength - marker.size()) { // No headers marker found inside the chunk. Treat the entire chunk as body. val bodyLength = chunkLength val body = Okio.buffer(FixedLengthSource(content, bodyLength)) diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/devsupport/MultipartStreamReaderTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/devsupport/MultipartStreamReaderTest.kt index 1ef4499184aa..8ede630a48a9 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/devsupport/MultipartStreamReaderTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/devsupport/MultipartStreamReaderTest.kt @@ -10,6 +10,8 @@ package com.facebook.react.devsupport import okio.Buffer import okio.BufferedSource import okio.ByteString +import okio.ForwardingSource +import okio.Okio import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -212,6 +214,43 @@ class MultipartStreamReaderTest { assertThat(callback.callCount).isEqualTo(1) } + @Test + @Suppress("DEPRECATION_ERROR") // Match the reader's compatibility with legacy Okio. + fun testDelimitersAcrossEveryReadBoundary() { + // The trailing CRLF must not become a header separator spanning the next boundary. + val body = "binary\u0000\r\n--samplX\r\n--sample-\r\n" + val response = "preamble\r\n--sample\r\n${body}\r\n--sample\r\nsecond\r\n--sample--\r\nepilogue" + for (readSize in 1..response.length) { + val upstream = Buffer().writeUtf8(response) + val source = + Okio.buffer( + object : ForwardingSource(upstream) { + override fun read(sink: Buffer, byteCount: Long): Long = + super.read(sink, minOf(byteCount, readSize.toLong())) + } + ) + val parts = mutableListOf() + val last = mutableListOf() + val success = + MultipartStreamReader(source, "sample") + .readAllParts( + object : CallCountTrackingChunkCallback() { + override fun onChunkComplete( + headers: Map, + body: BufferedSource, + isLastChunk: Boolean, + ) { + parts.add(body.readUtf8()) + last.add(isLastChunk) + } + } + ) + assertThat(success).describedAs("read size %s", readSize).isTrue() + assertThat(parts).containsExactly(body, "second") + assertThat(last).containsExactly(false, true) + } + } + internal open class CallCountTrackingChunkCallback : MultipartStreamReader.ChunkListener { var callCount = 0 private set