From d7b05334146497858fb7c89777680185261b97a5 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Wed, 14 Aug 2024 11:21:40 -0400 Subject: [PATCH] Propagate byte read/write counts from unsafe buffer ops (#364) * Propagate byte read/write counts from unsafe buffer ops * Include JVM-specific extensions as well This allows the caller to more easily perform their own bookkeeping based on the amount of bytes moved. Closes #360 --- core/api/kotlinx-io-core.api | 14 +++--- core/api/kotlinx-io-core.klib.api | 8 +-- .../src/unsafe/UnsafeBufferOperations.kt | 50 +++++++++++++------ .../unsafe/UnsafeBufferOperationsReadTest.kt | 20 +++++--- .../unsafe/UnsafeBufferOperationsWriteTest.kt | 19 ++++--- .../src/unsafe/UnsafeBufferOperationsJvm.kt | 33 ++++++++---- .../UnsafeBufferOperationsJvmReadBulkTest.kt | 10 ++-- ...safeBufferOperationsJvmReadFromHeadTest.kt | 15 ++++-- ...nsafeBufferOperationsJvmWriteToTailTest.kt | 16 +++--- 9 files changed, 118 insertions(+), 67 deletions(-) diff --git a/core/api/kotlinx-io-core.api b/core/api/kotlinx-io-core.api index 2662ef02f..35fc21e74 100644 --- a/core/api/kotlinx-io-core.api +++ b/core/api/kotlinx-io-core.api @@ -310,16 +310,16 @@ public final class kotlinx/io/unsafe/UnsafeBufferOperations { public final fun iterate (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function2;)V public final fun moveToTail (Lkotlinx/io/Buffer;[BII)V public static synthetic fun moveToTail$default (Lkotlinx/io/unsafe/UnsafeBufferOperations;Lkotlinx/io/Buffer;[BIIILjava/lang/Object;)V - public final fun readFromHead (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function2;)V - public final fun readFromHead (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function3;)V - public final fun writeToTail (Lkotlinx/io/Buffer;ILkotlin/jvm/functions/Function2;)V - public final fun writeToTail (Lkotlinx/io/Buffer;ILkotlin/jvm/functions/Function3;)V + public final fun readFromHead (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function2;)I + public final fun readFromHead (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function3;)I + public final fun writeToTail (Lkotlinx/io/Buffer;ILkotlin/jvm/functions/Function2;)I + public final fun writeToTail (Lkotlinx/io/Buffer;ILkotlin/jvm/functions/Function3;)I } public final class kotlinx/io/unsafe/UnsafeBufferOperationsJvmKt { - public static final fun readBulk (Lkotlinx/io/unsafe/UnsafeBufferOperations;Lkotlinx/io/Buffer;[Ljava/nio/ByteBuffer;Lkotlin/jvm/functions/Function2;)V - public static final fun readFromHead (Lkotlinx/io/unsafe/UnsafeBufferOperations;Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function1;)V - public static final fun writeToTail (Lkotlinx/io/unsafe/UnsafeBufferOperations;Lkotlinx/io/Buffer;ILkotlin/jvm/functions/Function1;)V + public static final fun readBulk (Lkotlinx/io/unsafe/UnsafeBufferOperations;Lkotlinx/io/Buffer;[Ljava/nio/ByteBuffer;Lkotlin/jvm/functions/Function2;)J + public static final fun readFromHead (Lkotlinx/io/unsafe/UnsafeBufferOperations;Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function1;)I + public static final fun writeToTail (Lkotlinx/io/unsafe/UnsafeBufferOperations;Lkotlinx/io/Buffer;ILkotlin/jvm/functions/Function1;)I } public final class kotlinx/io/unsafe/UnsafeBufferOperationsKt { diff --git a/core/api/kotlinx-io-core.klib.api b/core/api/kotlinx-io-core.klib.api index 08b6322c0..06a3309e6 100644 --- a/core/api/kotlinx-io-core.klib.api +++ b/core/api/kotlinx-io-core.klib.api @@ -213,10 +213,10 @@ final object kotlinx.io.unsafe/UnsafeBufferOperations { // kotlinx.io.unsafe/Uns final fun moveToTail(kotlinx.io/Buffer, kotlin/ByteArray, kotlin/Int = ..., kotlin/Int = ...) // kotlinx.io.unsafe/UnsafeBufferOperations.moveToTail|moveToTail(kotlinx.io.Buffer;kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] final inline fun iterate(kotlinx.io/Buffer, kotlin/Function2) // kotlinx.io.unsafe/UnsafeBufferOperations.iterate|iterate(kotlinx.io.Buffer;kotlin.Function2){}[0] final inline fun iterate(kotlinx.io/Buffer, kotlin/Long, kotlin/Function3) // kotlinx.io.unsafe/UnsafeBufferOperations.iterate|iterate(kotlinx.io.Buffer;kotlin.Long;kotlin.Function3){}[0] - final inline fun readFromHead(kotlinx.io/Buffer, kotlin/Function2) // kotlinx.io.unsafe/UnsafeBufferOperations.readFromHead|readFromHead(kotlinx.io.Buffer;kotlin.Function2){}[0] - final inline fun readFromHead(kotlinx.io/Buffer, kotlin/Function3) // kotlinx.io.unsafe/UnsafeBufferOperations.readFromHead|readFromHead(kotlinx.io.Buffer;kotlin.Function3){}[0] - final inline fun writeToTail(kotlinx.io/Buffer, kotlin/Int, kotlin/Function2) // kotlinx.io.unsafe/UnsafeBufferOperations.writeToTail|writeToTail(kotlinx.io.Buffer;kotlin.Int;kotlin.Function2){}[0] - final inline fun writeToTail(kotlinx.io/Buffer, kotlin/Int, kotlin/Function3) // kotlinx.io.unsafe/UnsafeBufferOperations.writeToTail|writeToTail(kotlinx.io.Buffer;kotlin.Int;kotlin.Function3){}[0] + final inline fun readFromHead(kotlinx.io/Buffer, kotlin/Function2): kotlin/Int // kotlinx.io.unsafe/UnsafeBufferOperations.readFromHead|readFromHead(kotlinx.io.Buffer;kotlin.Function2){}[0] + final inline fun readFromHead(kotlinx.io/Buffer, kotlin/Function3): kotlin/Int // kotlinx.io.unsafe/UnsafeBufferOperations.readFromHead|readFromHead(kotlinx.io.Buffer;kotlin.Function3){}[0] + final inline fun writeToTail(kotlinx.io/Buffer, kotlin/Int, kotlin/Function2): kotlin/Int // kotlinx.io.unsafe/UnsafeBufferOperations.writeToTail|writeToTail(kotlinx.io.Buffer;kotlin.Int;kotlin.Function2){}[0] + final inline fun writeToTail(kotlinx.io/Buffer, kotlin/Int, kotlin/Function3): kotlin/Int // kotlinx.io.unsafe/UnsafeBufferOperations.writeToTail|writeToTail(kotlinx.io.Buffer;kotlin.Int;kotlin.Function3){}[0] } final val kotlinx.io.files/SystemFileSystem // kotlinx.io.files/SystemFileSystem|{}SystemFileSystem[0] diff --git a/core/common/src/unsafe/UnsafeBufferOperations.kt b/core/common/src/unsafe/UnsafeBufferOperations.kt index 2415714d8..d1e9774c6 100644 --- a/core/common/src/unsafe/UnsafeBufferOperations.kt +++ b/core/common/src/unsafe/UnsafeBufferOperations.kt @@ -66,6 +66,7 @@ public object UnsafeBufferOperations { * and data from the consumed prefix will be no longer available for read. * If the operation does not consume anything, the action should return `0`. * It's considered an error to return a negative value or a value exceeding the size of a readable range. + * This value will also be propagated as the function return value. * * If [readAction] ends execution by throwing an exception, no data will be consumed from the buffer. * @@ -75,6 +76,8 @@ public object UnsafeBufferOperations { * the best effort basis, meaning that there are no strong zero-copy guarantees * and the copy will be created if it could not be omitted. * + * @return Number of bytes consumed as returned by [readAction]. + * * @throws IllegalStateException when [readAction] returns negative value or a values exceeding * the `endIndexExclusive - startIndexInclusive` value. * @throws IllegalArgumentException when the [buffer] is empty. @@ -84,14 +87,16 @@ public object UnsafeBufferOperations { public inline fun readFromHead( buffer: Buffer, readAction: (bytes: ByteArray, startIndexInclusive: Int, endIndexExclusive: Int) -> Int - ) { + ): Int { require(!buffer.exhausted()) { "Buffer is empty" } val head = buffer.head!! val bytesRead = readAction(head.dataAsByteArray(true), head.pos, head.limit) - if (bytesRead < 0) throw IllegalStateException("Returned negative read bytes count") - if (bytesRead == 0) return - if (bytesRead > head.size) throw IllegalStateException("Returned too many bytes") - buffer.skip(bytesRead.toLong()) + if (bytesRead != 0) { + if (bytesRead < 0) throw IllegalStateException("Returned negative read bytes count") + if (bytesRead > head.size) throw IllegalStateException("Returned too many bytes") + buffer.skip(bytesRead.toLong()) + } + return bytesRead } /** @@ -107,26 +112,31 @@ public object UnsafeBufferOperations { * and data from the consumed prefix will be no longer available for read. * If the operation does not consume anything, the action should return `0`. * It's considered an error to return a negative value or a value exceeding the [Segment.size]. + * This value will also be propagated as the function return value. * * Both [readAction] arguments are valid only within [readAction] scope, * it's an error to store and reuse it later. * * If the buffer is empty, [IllegalArgumentException] will be thrown. * + * @return Number of bytes consumed as returned by [readAction]. + * * @throws IllegalStateException when [readAction] returns negative value or a values exceeding * the [Segment.size] value. * @throws IllegalArgumentException when the [buffer] is empty. * * @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.readUleb128 */ - public inline fun readFromHead(buffer: Buffer, readAction: (SegmentReadContext, Segment) -> Int) { + public inline fun readFromHead(buffer: Buffer, readAction: (SegmentReadContext, Segment) -> Int): Int { require(!buffer.exhausted()) { "Buffer is empty" } val head = buffer.head!! val bytesRead = readAction(SegmentReadContextImpl, head) - if (bytesRead < 0) throw IllegalStateException("Returned negative read bytes count") - if (bytesRead == 0) return - if (bytesRead > head.size) throw IllegalStateException("Returned too many bytes") - buffer.skip(bytesRead.toLong()) + if (bytesRead != 0) { + if (bytesRead < 0) throw IllegalStateException("Returned negative read bytes count") + if (bytesRead > head.size) throw IllegalStateException("Returned too many bytes") + buffer.skip(bytesRead.toLong()) + } + return bytesRead } /** @@ -146,6 +156,7 @@ public object UnsafeBufferOperations { * The value returned by the [writeAction] denotes the number of bytes successfully written to the buffer. * If no data was written, `0` should be returned. * It's an error to return a negative value or a value exceeding the size of the provided writeable range. + * This value will also be propagated as the function return value. * * If [writeAction] ends execution by throwing an exception, no data will be written to the buffer. * @@ -153,6 +164,8 @@ public object UnsafeBufferOperations { * on the best-effort basis, meaning that there are no strong zero-copy guarantees * and the copy will be created if it could not be omitted. * + * @return Number of bytes written as returned by [writeAction]. + * * @throws IllegalStateException when [minimumCapacity] is too large and could not be fulfilled. * @throws IllegalStateException when [writeAction] returns a negative value or a value exceeding * the `endIndexExclusive - startIndexInclusive` value. @@ -162,7 +175,7 @@ public object UnsafeBufferOperations { public inline fun writeToTail( buffer: Buffer, minimumCapacity: Int, writeAction: (bytes: ByteArray, startIndexInclusive: Int, endIndexExclusive: Int) -> Int - ) { + ): Int { val tail = buffer.writableSegment(minimumCapacity) val data = tail.dataAsByteArray(false) @@ -175,7 +188,7 @@ public object UnsafeBufferOperations { tail.writeBackData(data, bytesWritten) tail.limit += bytesWritten buffer.sizeMut += bytesWritten - return + return bytesWritten } check(bytesWritten in 0..tail.remainingCapacity) { @@ -185,11 +198,12 @@ public object UnsafeBufferOperations { tail.writeBackData(data, bytesWritten) tail.limit += bytesWritten buffer.sizeMut += bytesWritten - return + return bytesWritten } if (tail.isEmpty()) { buffer.recycleTail() } + return bytesWritten } /** @@ -208,10 +222,13 @@ public object UnsafeBufferOperations { * The value returned by the [writeAction] denotes the number of bytes successfully written to the buffer. * If no data was written, `0` should be returned. * It's an error to return a negative value or a value exceeding the [Segment.remainingCapacity]. + * This value will also be propagated as the function return value. * * Both [writeAction] arguments are valid only within [writeAction] scope, * it's an error to store and reuse it later. * + * @return Number of bytes written as returned by [writeAction]. + * * @throws IllegalStateException when [minimumCapacity] is too large and could not be fulfilled. * @throws IllegalStateException when [writeAction] returns a negative value or a value exceeding * the [Segment.remainingCapacity] value for the provided segment. @@ -222,7 +239,7 @@ public object UnsafeBufferOperations { buffer: Buffer, minimumCapacity: Int, writeAction: (SegmentWriteContext, Segment) -> Int - ) { + ): Int { val tail = buffer.writableSegment(minimumCapacity) val bytesWritten = writeAction(SegmentWriteContextImpl, tail) @@ -230,7 +247,7 @@ public object UnsafeBufferOperations { if (bytesWritten == minimumCapacity) { tail.limit += bytesWritten buffer.sizeMut += bytesWritten - return + return bytesWritten } check(bytesWritten in 0..tail.remainingCapacity) { @@ -239,12 +256,13 @@ public object UnsafeBufferOperations { if (bytesWritten != 0) { tail.limit += bytesWritten buffer.sizeMut += bytesWritten - return + return bytesWritten } if (tail.isEmpty()) { buffer.recycleTail() } + return bytesWritten } /** diff --git a/core/common/test/unsafe/UnsafeBufferOperationsReadTest.kt b/core/common/test/unsafe/UnsafeBufferOperationsReadTest.kt index 64c941c6d..9ad7da928 100644 --- a/core/common/test/unsafe/UnsafeBufferOperationsReadTest.kt +++ b/core/common/test/unsafe/UnsafeBufferOperationsReadTest.kt @@ -13,6 +13,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue +import kotlin.test.fail @OptIn(UnsafeIoApi::class) class UnsafeBufferOperationsReadTest { @@ -38,10 +39,11 @@ class UnsafeBufferOperationsReadTest { val buffer = Buffer().apply { write(expectedData) } for (idx in actualData.indices) { - UnsafeBufferOperations.readFromHead(buffer) { data, startIndex, _ -> + val read = UnsafeBufferOperations.readFromHead(buffer) { data, startIndex, _ -> actualData[idx] = data[startIndex] 1 } + assertEquals(1, read) assertEquals(actualData.size - idx - 1, buffer.size.toInt()) } assertTrue(buffer.exhausted()) @@ -51,24 +53,28 @@ class UnsafeBufferOperationsReadTest { @Test fun readNothing() { val buffer = Buffer().apply { writeInt(42) } - UnsafeBufferOperations.readFromHead(buffer) { _, _, _ -> 0 } + val read1 = UnsafeBufferOperations.readFromHead(buffer) { _, _, _ -> 0 } + assertEquals(0, read1) assertEquals(42, buffer.readInt()) buffer.writeInt(42) - UnsafeBufferOperations.readFromHead(buffer) { _, _ -> 0 } + val read2 = UnsafeBufferOperations.readFromHead(buffer) { _, _ -> 0 } + assertEquals(0, read2) assertEquals(42, buffer.readInt()) } @Test fun readEverything() { val buffer = Buffer().apply { writeString("hello world") } - UnsafeBufferOperations.readFromHead(buffer) { _, startIndex, endIndex -> + val read1 = UnsafeBufferOperations.readFromHead(buffer) { _, startIndex, endIndex -> endIndex - startIndex } + assertEquals(11, read1) assertTrue(buffer.exhausted()) buffer.writeString("hello world") - UnsafeBufferOperations.readFromHead(buffer) { _, seg -> seg.size } + val read2 = UnsafeBufferOperations.readFromHead(buffer) { _, seg -> seg.size } + assertEquals(11, read2) assertTrue(buffer.exhausted()) } @@ -76,11 +82,11 @@ class UnsafeBufferOperationsReadTest { fun readFromEmptyBuffer() { val buffer = Buffer() assertFailsWith { - UnsafeBufferOperations.readFromHead(buffer) { _, _, _ -> 0 } + UnsafeBufferOperations.readFromHead(buffer) { _, _, _ -> fail() } } assertFailsWith { - UnsafeBufferOperations.readFromHead(buffer) { _, _ -> 0 } + UnsafeBufferOperations.readFromHead(buffer) { _, _ -> fail() } } } diff --git a/core/common/test/unsafe/UnsafeBufferOperationsWriteTest.kt b/core/common/test/unsafe/UnsafeBufferOperationsWriteTest.kt index 164c312d2..3a92108c6 100644 --- a/core/common/test/unsafe/UnsafeBufferOperationsWriteTest.kt +++ b/core/common/test/unsafe/UnsafeBufferOperationsWriteTest.kt @@ -38,10 +38,11 @@ class UnsafeBufferOperationsWriteTest { val data = "hello world".encodeToByteArray() for (idx in data.indices) { - UnsafeBufferOperations.writeToTail(buffer, 1) { writeable, pos, _ -> + val written = UnsafeBufferOperations.writeToTail(buffer, 1) { writeable, pos, _ -> writeable[pos] = data[idx] 1 } + assertEquals(1, written) assertEquals(idx + 1, buffer.size.toInt()) } assertEquals("hello world", buffer.readString()) @@ -51,10 +52,12 @@ class UnsafeBufferOperationsWriteTest { fun writeNothing() { val buffer = Buffer() - UnsafeBufferOperations.writeToTail(buffer, 1) { _, _, _ -> 0 } + val write1 = UnsafeBufferOperations.writeToTail(buffer, 1) { _, _, _ -> 0 } + assertEquals(0, write1) assertTrue(buffer.exhausted()) - UnsafeBufferOperations.writeToTail(buffer, 1) { _, _ -> 0 } + val write2 = UnsafeBufferOperations.writeToTail(buffer, 1) { _, _ -> 0 } + assertEquals(0, write2) assertTrue(buffer.exhausted()) buffer.writeInt(42) @@ -75,12 +78,13 @@ class UnsafeBufferOperationsWriteTest { @Test fun writeWholeBuffer() { val buffer = Buffer() - UnsafeBufferOperations.writeToTail(buffer, 1) { data, from, to -> + val written = UnsafeBufferOperations.writeToTail(buffer, 1) { data, from, to -> for (idx in from.. + val written = UnsafeBufferOperations.writeToTail(buffer, 1) { ctx, segment -> ctx.setUnchecked(segment, 0, 1) ctx.setUnchecked(segment, 1, 2) 2 } + assertEquals(2, written) assertArrayEquals(byteArrayOf(1, 2), buffer.readByteArray()) } @@ -102,12 +107,12 @@ class UnsafeBufferOperationsWriteTest { fun requireToManyBytes() { val buffer = Buffer() assertFailsWith { - UnsafeBufferOperations.writeToTail(buffer, 100500) { _, _, _ -> 0 } + UnsafeBufferOperations.writeToTail(buffer, 100500) { _, _, _ -> fail() } } assertTrue(buffer.exhausted()) assertFailsWith { - UnsafeBufferOperations.writeToTail(buffer, 100500) { _, _ -> 0 } + UnsafeBufferOperations.writeToTail(buffer, 100500) { _, _ -> fail() } } assertTrue(buffer.exhausted()) } diff --git a/core/jvm/src/unsafe/UnsafeBufferOperationsJvm.kt b/core/jvm/src/unsafe/UnsafeBufferOperationsJvm.kt index b0fb6f550..67a82d541 100644 --- a/core/jvm/src/unsafe/UnsafeBufferOperationsJvm.kt +++ b/core/jvm/src/unsafe/UnsafeBufferOperationsJvm.kt @@ -19,6 +19,7 @@ import java.nio.ByteBuffer * * After exiting the [readAction], all data consumed from the [ByteBuffer] will be also consumed from the [buffer]. * Consumed bytes determined as a difference between [ByteBuffer.capacity] and [ByteBuffer.remaining]. + * This value will also be propagated as the function return value. * * If [readAction] ends execution by throwing an exception, no data will be consumed from the buffer. * @@ -31,13 +32,15 @@ import java.nio.ByteBuffer * @param buffer a buffer to read from * @param readAction an action that will be invoked on a [ByteBuffer] containing data from [buffer]'s head * + * @return Number of bytes read by [readAction]. + * * @throws IllegalArgumentException when the [buffer] is empty. * * @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.writeToByteChannel */ @UnsafeIoApi -public inline fun UnsafeBufferOperations.readFromHead(buffer: Buffer, readAction: (ByteBuffer) -> Unit) { - readFromHead(buffer) { rawData, pos, limit -> +public inline fun UnsafeBufferOperations.readFromHead(buffer: Buffer, readAction: (ByteBuffer) -> Unit): Int { + return readFromHead(buffer) { rawData, pos, limit -> val bb = ByteBuffer.wrap(rawData, pos, limit - pos).slice().asReadOnlyBuffer() readAction(bb) bb.position() @@ -58,6 +61,7 @@ public inline fun UnsafeBufferOperations.readFromHead(buffer: Buffer, readAction * * After exiting [writeAction], bytes written to the [ByteBuffer] will be committed to the buffer. * The number of bytes written is determined as a difference between [ByteBuffer.capacity] and [ByteBuffer.remaining]. + * This value will also be propagated as the function return value. * * If [writeAction] ends execution by throwing an exception, no data will be written to the buffer. * @@ -70,6 +74,8 @@ public inline fun UnsafeBufferOperations.readFromHead(buffer: Buffer, readAction * @param writeAction an action that will be invoked on a [ByteBuffer] that will be added to a [buffer] by the end of * the call * + * @return Number of bytes written by [writeAction]. + * * @throws IllegalStateException when [minimumCapacity] is too large and could not be fulfilled. * * @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.readFromByteChannel @@ -79,8 +85,8 @@ public inline fun UnsafeBufferOperations.writeToTail( buffer: Buffer, minimumCapacity: Int, writeAction: (ByteBuffer) -> Unit -) { - writeToTail(buffer, minimumCapacity) { rawData, pos, limit -> +): Int { + return writeToTail(buffer, minimumCapacity) { rawData, pos, limit -> val bb = ByteBuffer.wrap(rawData, pos, limit - pos).slice() writeAction(bb) bb.position() @@ -102,6 +108,7 @@ public inline fun UnsafeBufferOperations.writeToTail( * The size of the buffer will be reduced by that value, * and the corresponding number of bytes from buffer's prefix will be no longer available for read. * If data was not consumed, the [readAction] should return `0`. + * This value will also be propagated as the function return value. * * If [readAction] ends execution by throwing an exception, no data will be consumed from the buffer. * @@ -118,6 +125,8 @@ public inline fun UnsafeBufferOperations.writeToTail( * @param readAction an action that will be invoked on an array filled with [ByteBuffer]s holding data from [buffer]'s * prefix * + * @return Number of bytes read as returned by [readAction]. + * * @throws IllegalArgumentException when the [buffer] is empty. * @throws IllegalArgumentException when the [iovec] is empty. * @@ -129,7 +138,7 @@ public inline fun UnsafeBufferOperations.readBulk( buffer: Buffer, iovec: Array, readAction: (iovec: Array, iovecSize: Int) -> Long -) { +): Long { val head = buffer.head ?: throw IllegalArgumentException("buffer is empty.") if (iovec.isEmpty()) throw IllegalArgumentException("iovec is empty.") @@ -148,11 +157,13 @@ public inline fun UnsafeBufferOperations.readBulk( } while (idx < iovec.size) val bytesRead = readAction(iovec, idx) - if (bytesRead == 0L) return - if (bytesRead < 0 || bytesRead > capacity) { - throw IllegalStateException( - "readAction should return a value in range [0, $capacity], but returned: $bytesRead" - ) + if (bytesRead != 0L) { + if (bytesRead < 0 || bytesRead > capacity) { + throw IllegalStateException( + "readAction should return a value in range [0, $capacity], but returned: $bytesRead" + ) + } + buffer.skip(bytesRead) } - buffer.skip(bytesRead) + return bytesRead } diff --git a/core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadBulkTest.kt b/core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadBulkTest.kt index 22084f36c..dcb67ec5f 100644 --- a/core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadBulkTest.kt +++ b/core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadBulkTest.kt @@ -16,7 +16,7 @@ class UnsafeBufferOperationsJvmReadBulkTest { @Test fun readAllFromEmptyBuffer() { assertFailsWith { - UnsafeBufferOperations.readBulk(Buffer(), Array(1) { null }) { _, _ -> 0L } + UnsafeBufferOperations.readBulk(Buffer(), Array(1) { null }) { _, _ -> fail() } } } @@ -25,7 +25,7 @@ class UnsafeBufferOperationsJvmReadBulkTest { assertFailsWith { UnsafeBufferOperations.readBulk( Buffer().apply { writeByte(0) }, - Array(0) { null }) { _, _ -> 0L } + Array(0) { null }) { _, _ -> fail() } } } @@ -34,7 +34,7 @@ class UnsafeBufferOperationsJvmReadBulkTest { val buffer = Buffer().apply { writeString("hello world") } val array = Array(16) { null } - UnsafeBufferOperations.readBulk(buffer, array) { arrayArg, iovecLen -> + val read = UnsafeBufferOperations.readBulk(buffer, array) { arrayArg, iovecLen -> assertSame(array, arrayArg) assertEquals(1, iovecLen) @@ -50,6 +50,7 @@ class UnsafeBufferOperationsJvmReadBulkTest { 11 } + assertEquals(11L, read) assertTrue(buffer.exhausted()) } @@ -58,7 +59,7 @@ class UnsafeBufferOperationsJvmReadBulkTest { val buffer = Buffer().apply { writeString("hello world") } val array = Array(16) { null } - UnsafeBufferOperations.readBulk(buffer, array) { arrayArg, iovecLen -> + val read = UnsafeBufferOperations.readBulk(buffer, array) { arrayArg, iovecLen -> assertSame(array, arrayArg) assertEquals(1, iovecLen) @@ -74,6 +75,7 @@ class UnsafeBufferOperationsJvmReadBulkTest { 0 } + assertEquals(0L, read) assertEquals("hello world", buffer.readString()) } diff --git a/core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadFromHeadTest.kt b/core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadFromHeadTest.kt index a232a0378..a264f7a08 100644 --- a/core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadFromHeadTest.kt +++ b/core/jvm/test/unsafe/UnsafeBufferOperationsJvmReadFromHeadTest.kt @@ -14,6 +14,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue +import kotlin.test.fail @OptIn(UnsafeIoApi::class) class UnsafeBufferOperationsJvmReadFromHeadTest { @@ -38,9 +39,10 @@ class UnsafeBufferOperationsJvmReadFromHeadTest { val buffer = Buffer().apply { write(expectedData) } for (idx in actualData.indices) { - UnsafeBufferOperations.readFromHead(buffer) { bb -> + val read = UnsafeBufferOperations.readFromHead(buffer) { bb -> actualData[idx] = bb.get() } + assertEquals(1, read) assertEquals(actualData.size - idx - 1, buffer.size.toInt()) } assertTrue(buffer.exhausted()) @@ -50,16 +52,18 @@ class UnsafeBufferOperationsJvmReadFromHeadTest { @Test fun readNothing() { val buffer = Buffer().apply { writeInt(42) } - UnsafeBufferOperations.readFromHead(buffer) { _ -> /* do nothing */ } + val read = UnsafeBufferOperations.readFromHead(buffer) { _ -> /* do nothing */ } + assertEquals(0, read) assertEquals(42, buffer.readInt()) } @Test fun readEverything() { val buffer = Buffer().apply { writeString("hello world") } - UnsafeBufferOperations.readFromHead(buffer) { bb -> + val read = UnsafeBufferOperations.readFromHead(buffer) { bb -> bb.position(bb.limit()) } + assertEquals(11, read) assertTrue(buffer.exhausted()) } @@ -78,7 +82,7 @@ class UnsafeBufferOperationsJvmReadFromHeadTest { fun readFromEmptyBuffer() { val buffer = Buffer() assertFailsWith { - UnsafeBufferOperations.readFromHead(buffer) { _ -> } + UnsafeBufferOperations.readFromHead(buffer) { _ -> fail() } } } @@ -104,11 +108,12 @@ class UnsafeBufferOperationsJvmReadFromHeadTest { @Test fun changeLimit() { val buffer = Buffer().apply { writeString("hello world") } - UnsafeBufferOperations.readFromHead(buffer) { bb -> + val read = UnsafeBufferOperations.readFromHead(buffer) { bb -> // read a single byte only bb.position(1) bb.limit(2) } + assertEquals(1, read) assertEquals(10, buffer.size) } diff --git a/core/jvm/test/unsafe/UnsafeBufferOperationsJvmWriteToTailTest.kt b/core/jvm/test/unsafe/UnsafeBufferOperationsJvmWriteToTailTest.kt index 4b881b9a4..e20aa4058 100644 --- a/core/jvm/test/unsafe/UnsafeBufferOperationsJvmWriteToTailTest.kt +++ b/core/jvm/test/unsafe/UnsafeBufferOperationsJvmWriteToTailTest.kt @@ -10,6 +10,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue +import kotlin.test.fail @OptIn(UnsafeIoApi::class) class UnsafeBufferOperationsJvmWriteToTailTest { @@ -33,9 +34,10 @@ class UnsafeBufferOperationsJvmWriteToTailTest { val data = "hello world".encodeToByteArray() for (idx in data.indices) { - UnsafeBufferOperations.writeToTail(buffer, 1) { bb -> + val written = UnsafeBufferOperations.writeToTail(buffer, 1) { bb -> bb.put(data[idx]) } + assertEquals(1, written) assertEquals(idx + 1, buffer.size.toInt()) } assertEquals("hello world", buffer.readString()) @@ -44,16 +46,18 @@ class UnsafeBufferOperationsJvmWriteToTailTest { @Test fun writeNothing() { val buffer = Buffer() - UnsafeBufferOperations.writeToTail(buffer, 1) { _ -> } + val written = UnsafeBufferOperations.writeToTail(buffer, 1) { _ -> } + assertEquals(0, written) assertTrue(buffer.exhausted()) } @Test fun writeWholeBuffer() { val buffer = Buffer() - UnsafeBufferOperations.writeToTail(buffer, 1) { bb -> + val written = UnsafeBufferOperations.writeToTail(buffer, 1) { bb -> bb.position(bb.limit()) } + assertEquals(Segment.SIZE, written) assertEquals(Segment.SIZE, buffer.size.toInt()) } @@ -61,7 +65,7 @@ class UnsafeBufferOperationsJvmWriteToTailTest { fun requireToManyBytes() { val buffer = Buffer() assertFailsWith { - UnsafeBufferOperations.writeToTail(buffer, 100500) { _ -> } + UnsafeBufferOperations.writeToTail(buffer, 100500) { _ -> fail() } } assertTrue(buffer.exhausted()) } @@ -87,12 +91,12 @@ class UnsafeBufferOperationsJvmWriteToTailTest { fun changeLimit() { val buffer = Buffer() - UnsafeBufferOperations.writeToTail(buffer, 8) { bb -> + val written = UnsafeBufferOperations.writeToTail(buffer, 8) { bb -> // only two bytes written bb.position(2) bb.limit(4) } - + assertEquals(2, written) assertEquals(2, buffer.size) }