From 613f51b7292dd6e3fc9b8ae719f261b3248a4daa Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Thu, 5 Sep 2024 15:47:54 -0400 Subject: [PATCH 1/3] Read/write ByteString from/to ByteBuffer Closes #269 --- bytestring/api/kotlinx-io-bytestring.api | 6 + bytestring/jvm/src/ByteStringJvmExt.kt | 104 ++++++++++++++++++ .../test/ByteStringByteBufferExtensions.kt | 98 +++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 bytestring/jvm/test/ByteStringByteBufferExtensions.kt diff --git a/bytestring/api/kotlinx-io-bytestring.api b/bytestring/api/kotlinx-io-bytestring.api index b51c6fc9c..8c5359bc9 100644 --- a/bytestring/api/kotlinx-io-bytestring.api +++ b/bytestring/api/kotlinx-io-bytestring.api @@ -64,8 +64,14 @@ public final class kotlinx/io/bytestring/ByteStringBuilderKt { } public final class kotlinx/io/bytestring/ByteStringJvmExtKt { + public static final fun asReadOnlyByteBuffer (Lkotlinx/io/bytestring/ByteString;)Ljava/nio/ByteBuffer; public static final fun decodeToString (Lkotlinx/io/bytestring/ByteString;Ljava/nio/charset/Charset;)Ljava/lang/String; public static final fun encodeToByteString (Ljava/lang/String;Ljava/nio/charset/Charset;)Lkotlinx/io/bytestring/ByteString; + public static final fun getByteString (Ljava/nio/ByteBuffer;I)Lkotlinx/io/bytestring/ByteString; + public static final fun getByteString (Ljava/nio/ByteBuffer;II)Lkotlinx/io/bytestring/ByteString; + public static synthetic fun getByteString$default (Ljava/nio/ByteBuffer;IILjava/lang/Object;)Lkotlinx/io/bytestring/ByteString; + public static final fun putByteString (Ljava/nio/ByteBuffer;ILkotlinx/io/bytestring/ByteString;)V + public static final fun putByteString (Ljava/nio/ByteBuffer;Lkotlinx/io/bytestring/ByteString;)V } public final class kotlinx/io/bytestring/ByteStringKt { diff --git a/bytestring/jvm/src/ByteStringJvmExt.kt b/bytestring/jvm/src/ByteStringJvmExt.kt index 555e65f86..d149e113c 100644 --- a/bytestring/jvm/src/ByteStringJvmExt.kt +++ b/bytestring/jvm/src/ByteStringJvmExt.kt @@ -5,6 +5,10 @@ package kotlinx.io.bytestring +import kotlinx.io.bytestring.unsafe.UnsafeByteStringApi +import kotlinx.io.bytestring.unsafe.UnsafeByteStringOperations +import java.nio.BufferOverflowException +import java.nio.ByteBuffer import java.nio.charset.Charset /** @@ -20,3 +24,103 @@ public fun ByteString.decodeToString(charset: Charset): String = getBackingArray * @param charset the encoding. */ public fun String.encodeToByteString(charset: Charset): ByteString = ByteString.wrap(toByteArray(charset)) + +/** + * Returns a new read-only heap [ByteBuffer] wrapping [this] ByteString's content. + */ +@OptIn(UnsafeByteStringApi::class) +public fun ByteString.asReadOnlyByteBuffer(): ByteBuffer { + val data: ByteArray + + UnsafeByteStringOperations.withByteArrayUnsafe(this) { + data = it + } + + return ByteBuffer.wrap(data).asReadOnlyBuffer() +} + +/** + * Reads [length] bytes of data from [this] ByteBuffer starting from the current position and + * wraps them into a new [ByteString]. + * + * Upon successful execution, current position will advance by [length]. + * + * @throws IndexOutOfBoundsException when [length] has negative value or its value exceeds [ByteBuffer.remaining] + */ +@OptIn(UnsafeByteStringApi::class) +public fun ByteBuffer.getByteString(length: Int = remaining()): ByteString { + if (length < 0) { + throw IndexOutOfBoundsException("length should be non-negative (was $length)") + } + if (remaining() < length) { + throw IndexOutOfBoundsException("length ($length) exceeds remaining bytes count ({${remaining()}})") + } + val bytes = ByteArray(length) + get(bytes) + return UnsafeByteStringOperations.wrapUnsafe(bytes) +} + +/** + * Reads [length] bytes of data from [this] ByteBuffer starting from [at] index and + * wraps them into a new [ByteString]. + * + * This function does not update [ByteBuffer.position]. + * + * @throws IndexOutOfBoundsException when [at] is negative, greater or equal to [ByteBuffer.limit] + * or [at] + [length] exceeds [ByteBuffer.limit]. + */ +@OptIn(UnsafeByteStringApi::class) +public fun ByteBuffer.getByteString(at: Int, length: Int): ByteString { + checkIndexAndCapacity(at, length) + val bytes = ByteArray(length) + // Absolute get(byte[]) was added only in JDK 13 + for (i in 0..= limit()) { + throw IndexOutOfBoundsException("Index $idx is out of this ByteBuffer's bounds: [0, ${limit()})") + } + if (length < 0) { + throw IndexOutOfBoundsException("length should be non-negative (was $length)") + } + if (idx + length > limit()) { + throw IndexOutOfBoundsException("There's not enough space to put ByteString of length $length starting" + + " from index $idx") + } +} \ No newline at end of file diff --git a/bytestring/jvm/test/ByteStringByteBufferExtensions.kt b/bytestring/jvm/test/ByteStringByteBufferExtensions.kt new file mode 100644 index 000000000..b80c1ccca --- /dev/null +++ b/bytestring/jvm/test/ByteStringByteBufferExtensions.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. + */ + +package kotlinx.io.bytestring + +import org.junit.jupiter.api.Test +import java.nio.BufferOverflowException +import java.nio.ByteBuffer +import java.nio.ReadOnlyBufferException +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +public class ByteStringByteBufferExtensions { + @Test + fun asReadOnlyByteBuffer() { + val buffer = ByteString(1, 2, 3, 4).asReadOnlyByteBuffer() + + assertTrue(buffer.isReadOnly) + assertEquals(4, buffer.remaining()) + + ByteArray(4).let { + buffer.get(it) + assertContentEquals(byteArrayOf(1, 2, 3, 4), it) + } + } + + @Test + fun getByteString() { + val bb = ByteBuffer.allocate(8) + bb.put(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8)) + bb.flip() + + assertEquals(ByteString(1, 2, 3, 4, 5, 6, 7, 8), bb.getByteString()) + bb.flip() + + assertEquals(ByteString(1, 2, 3, 4), bb.getByteString(length = 4)) + assertEquals(ByteString(), bb.getByteString(length = 0)) + assertFailsWith { bb.getByteString(length = -1) } + val p = bb.position() + assertFailsWith { bb.getByteString(length = 5) } + assertEquals(p, bb.position()) + bb.clear() + + assertEquals(ByteString(1, 2, 3, 4, 5, 6, 7, 8), bb.getByteString(at = 0, length = 8)) + assertEquals(0, bb.position()) + + assertEquals(ByteString(2, 3, 4, 5), bb.getByteString(at = 1, length = 4)) + assertEquals(0, bb.position()) + + assertFailsWith { bb.getByteString(at = -1, length = 8) } + assertFailsWith { bb.getByteString(at = 9, length = 1) } + assertFailsWith { bb.getByteString(at = 7, length = 2) } + assertFailsWith { bb.getByteString(at = 0, length = -1) } + } + + @Test + fun putString() { + val bb = ByteBuffer.allocate(8) + val string = ByteString(1, 2, 3, 4, 5, 6, 7, 8) + val shortString = ByteString(-1, -2, -3) + + bb.putByteString(string) + assertEquals(8, bb.position()) + bb.flip() + ByteArray(8).let { + bb.get(it) + assertContentEquals(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8), it) + } + + bb.clear() + bb.position(1) + assertFailsWith { bb.putByteString(string) } + assertEquals(1, bb.position()) + + bb.putByteString(at = 0, string = shortString) + bb.putByteString(at = 5, string = shortString) + assertEquals(1, bb.position()) + bb.clear() + ByteArray(8).let { + bb.get(it) + assertContentEquals(byteArrayOf(-1, -2, -3, 4, 5, -1, -2, -3), it) + } + + assertFailsWith { bb.putByteString(at = 7, string = shortString) } + assertFailsWith { bb.putByteString(at = -1, string = string) } + assertFailsWith { bb.putByteString(at = 8, string = string) } + assertFailsWith { + bb.asReadOnlyBuffer().putByteString(string) + } + assertFailsWith { + bb.asReadOnlyBuffer().putByteString(at = 0, string = string) + } + } +} \ No newline at end of file From bc6285d62cb345dde85d321a85368af83310f30d Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 6 Sep 2024 09:29:37 -0400 Subject: [PATCH 2/3] Fixed a copy-paste typo Co-authored-by: Jake Wharton --- bytestring/jvm/src/ByteStringJvmExt.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bytestring/jvm/src/ByteStringJvmExt.kt b/bytestring/jvm/src/ByteStringJvmExt.kt index d149e113c..7dfddec97 100644 --- a/bytestring/jvm/src/ByteStringJvmExt.kt +++ b/bytestring/jvm/src/ByteStringJvmExt.kt @@ -106,7 +106,7 @@ public fun ByteBuffer.putByteString(string: ByteString) { */ public fun ByteBuffer.putByteString(at: Int, string: ByteString) { checkIndexAndCapacity(at, string.size) - // Absolute get(byte[]) was added only in JDK 13 + // Absolute put(byte[]) was added only in JDK 16 for (idx in string.indices) { put(at + idx, string[idx]) } From ff6d18a30e50d504127f2cd2c5c8e44534e0c5a0 Mon Sep 17 00:00:00 2001 From: Filipp Zhinkin Date: Fri, 6 Sep 2024 10:38:18 -0400 Subject: [PATCH 3/3] Added samples --- bytestring/jvm/src/ByteStringJvmExt.kt | 12 ++- ... => ByteStringByteBufferExtensionsTest.kt} | 4 +- bytestring/jvm/test/samples/samplesJvm.kt | 78 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) rename bytestring/jvm/test/{ByteStringByteBufferExtensions.kt => ByteStringByteBufferExtensionsTest.kt} (98%) create mode 100644 bytestring/jvm/test/samples/samplesJvm.kt diff --git a/bytestring/jvm/src/ByteStringJvmExt.kt b/bytestring/jvm/src/ByteStringJvmExt.kt index 7dfddec97..ca193ac37 100644 --- a/bytestring/jvm/src/ByteStringJvmExt.kt +++ b/bytestring/jvm/src/ByteStringJvmExt.kt @@ -27,6 +27,8 @@ public fun String.encodeToByteString(charset: Charset): ByteString = ByteString. /** * Returns a new read-only heap [ByteBuffer] wrapping [this] ByteString's content. + * + * @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.toReadOnlyByteBuffer */ @OptIn(UnsafeByteStringApi::class) public fun ByteString.asReadOnlyByteBuffer(): ByteBuffer { @@ -46,6 +48,8 @@ public fun ByteString.asReadOnlyByteBuffer(): ByteBuffer { * Upon successful execution, current position will advance by [length]. * * @throws IndexOutOfBoundsException when [length] has negative value or its value exceeds [ByteBuffer.remaining] + * + * @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.getByteStringFromBuffer */ @OptIn(UnsafeByteStringApi::class) public fun ByteBuffer.getByteString(length: Int = remaining()): ByteString { @@ -68,6 +72,8 @@ public fun ByteBuffer.getByteString(length: Int = remaining()): ByteString { * * @throws IndexOutOfBoundsException when [at] is negative, greater or equal to [ByteBuffer.limit] * or [at] + [length] exceeds [ByteBuffer.limit]. + * + * @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.getByteStringFromBufferAbsolute */ @OptIn(UnsafeByteStringApi::class) public fun ByteBuffer.getByteString(at: Int, length: Int): ByteString { @@ -87,6 +93,8 @@ public fun ByteBuffer.getByteString(at: Int, length: Int): ByteString { * * @throws java.nio.ReadOnlyBufferException when [this] buffer is read-only * @throws java.nio.BufferOverflowException when [string] can't fit into remaining space of this buffer + * + * @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.putByteStringToBuffer */ @OptIn(UnsafeByteStringApi::class) public fun ByteBuffer.putByteString(string: ByteString) { @@ -103,6 +111,8 @@ public fun ByteBuffer.putByteString(string: ByteString) { * @throws java.nio.ReadOnlyBufferException when [this] buffer is read-only * @throws IndexOutOfBoundsException when [at] is negative, exceeds [ByteBuffer.limit], or * [at] + [ByteString.size] exceeds [ByteBuffer.limit] + * + * @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.putByteStringToBufferAbsolute */ public fun ByteBuffer.putByteString(at: Int, string: ByteString) { checkIndexAndCapacity(at, string.size) @@ -123,4 +133,4 @@ private fun ByteBuffer.checkIndexAndCapacity(idx: Int, length: Int) { throw IndexOutOfBoundsException("There's not enough space to put ByteString of length $length starting" + " from index $idx") } -} \ No newline at end of file +} diff --git a/bytestring/jvm/test/ByteStringByteBufferExtensions.kt b/bytestring/jvm/test/ByteStringByteBufferExtensionsTest.kt similarity index 98% rename from bytestring/jvm/test/ByteStringByteBufferExtensions.kt rename to bytestring/jvm/test/ByteStringByteBufferExtensionsTest.kt index b80c1ccca..1a08ae369 100644 --- a/bytestring/jvm/test/ByteStringByteBufferExtensions.kt +++ b/bytestring/jvm/test/ByteStringByteBufferExtensionsTest.kt @@ -14,7 +14,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue -public class ByteStringByteBufferExtensions { +public class ByteStringByteBufferExtensionsTest { @Test fun asReadOnlyByteBuffer() { val buffer = ByteString(1, 2, 3, 4).asReadOnlyByteBuffer() @@ -95,4 +95,4 @@ public class ByteStringByteBufferExtensions { bb.asReadOnlyBuffer().putByteString(at = 0, string = string) } } -} \ No newline at end of file +} diff --git a/bytestring/jvm/test/samples/samplesJvm.kt b/bytestring/jvm/test/samples/samplesJvm.kt new file mode 100644 index 000000000..ee97d186f --- /dev/null +++ b/bytestring/jvm/test/samples/samplesJvm.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file. + */ + +package kotlinx.io.bytestring.samples + +import kotlinx.io.bytestring.* +import java.nio.ByteBuffer +import java.nio.ReadOnlyBufferException +import kotlin.test.* + +public class ByteStringSamplesJvm { + @Test + fun toReadOnlyByteBuffer() { + val str = "Hello World".encodeToByteString() + val buffer = str.asReadOnlyByteBuffer() + + assertEquals(11, buffer.remaining()) + assertEquals(0x48656c6c, buffer.getInt()) + + buffer.flip() + assertFailsWith { buffer.put(42) } + } + + @Test + fun getByteStringFromBuffer() { + val buffer = ByteBuffer.wrap("Hello World".encodeToByteArray()) + + // Consume the whole buffer + val byteString = buffer.getByteString() + assertEquals(0, buffer.remaining()) + assertEquals("Hello World".encodeToByteString(), byteString) + + // Reset the buffer + buffer.flip() + // Consume only first 5 bytes from the buffer + assertEquals("Hello".encodeToByteString(), buffer.getByteString(length = 5)) + } + + @Test + fun getByteStringFromBufferAbsolute() { + val buffer = ByteBuffer.wrap("Hello World".encodeToByteArray()) + + // Read 2 bytes starting from offset 6 + val byteString = buffer.getByteString(at = 6, length = 2) + // Buffer's position is not affected + assertEquals(11, buffer.remaining()) + assertEquals(byteString, "Wo".encodeToByteString()) + } + + @Test + fun putByteStringToBuffer() { + val buffer = ByteBuffer.allocate(32) + val byteString = ByteString(0x66, 0xdb.toByte(), 0x11, 0x50) + + // Putting a ByteString into a buffer will advance its position + buffer.putByteString(byteString) + assertEquals(4, buffer.position()) + + buffer.flip() + assertEquals(1725632848, buffer.getInt()) + } + + @Test + fun putByteStringToBufferAbsolute() { + val buffer = ByteBuffer.allocate(8) + val byteString = ByteString(0x78, 0x5e) + + // Putting a ByteString into a buffer using an absolute offset + // won't change buffer's position. + buffer.putByteString(at = 3, string = byteString) + assertEquals(0, buffer.position()) + assertEquals(8, buffer.remaining()) + + assertEquals(0x000000785e000000L, buffer.getLong()) + } +}