Skip to content

Commit

Permalink
Read/write ByteString from/to ByteBuffer (#387)
Browse files Browse the repository at this point in the history
* Read/write ByteString from/to ByteBuffer

Closes #269

---------

Co-authored-by: Jake Wharton <[email protected]>
  • Loading branch information
fzhinkin and JakeWharton authored Sep 12, 2024
1 parent 796a458 commit 9db9bd6
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 0 deletions.
6 changes: 6 additions & 0 deletions bytestring/api/kotlinx-io-bytestring.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
114 changes: 114 additions & 0 deletions bytestring/jvm/src/ByteStringJvmExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -20,3 +24,113 @@ 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.
*
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.toReadOnlyByteBuffer
*/
@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]
*
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.getByteStringFromBuffer
*/
@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].
*
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.getByteStringFromBufferAbsolute
*/
@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..<length) {
bytes[i] = get(at + i)
}
return UnsafeByteStringOperations.wrapUnsafe(bytes)
}

/**
* Writes [string] into [this] ByteBuffer starting from the current position.
*
* Upon successfully execution [ByteBuffer.position] will advance by the length of [string].
*
* @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) {
UnsafeByteStringOperations.withByteArrayUnsafe(string) {
put(it)
}
}

/**
* Writes [string] into [this] ByteBuffer starting from position [at].
*
* This function does not update [ByteBuffer.position].
*
* @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)
// Absolute put(byte[]) was added only in JDK 16
for (idx in string.indices) {
put(at + idx, string[idx])
}
}

private fun ByteBuffer.checkIndexAndCapacity(idx: Int, length: Int) {
if (idx < 0 || idx >= 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")
}
}
98 changes: 98 additions & 0 deletions bytestring/jvm/test/ByteStringByteBufferExtensionsTest.kt
Original file line number Diff line number Diff line change
@@ -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 ByteStringByteBufferExtensionsTest {
@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<IndexOutOfBoundsException> { bb.getByteString(length = -1) }
val p = bb.position()
assertFailsWith<IndexOutOfBoundsException> { 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<IndexOutOfBoundsException> { bb.getByteString(at = -1, length = 8) }
assertFailsWith<IndexOutOfBoundsException> { bb.getByteString(at = 9, length = 1) }
assertFailsWith<IndexOutOfBoundsException> { bb.getByteString(at = 7, length = 2) }
assertFailsWith<IndexOutOfBoundsException> { 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<BufferOverflowException> { 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<IndexOutOfBoundsException> { bb.putByteString(at = 7, string = shortString) }
assertFailsWith<IndexOutOfBoundsException> { bb.putByteString(at = -1, string = string) }
assertFailsWith<IndexOutOfBoundsException> { bb.putByteString(at = 8, string = string) }
assertFailsWith<ReadOnlyBufferException> {
bb.asReadOnlyBuffer().putByteString(string)
}
assertFailsWith<ReadOnlyBufferException> {
bb.asReadOnlyBuffer().putByteString(at = 0, string = string)
}
}
}
78 changes: 78 additions & 0 deletions bytestring/jvm/test/samples/samplesJvm.kt
Original file line number Diff line number Diff line change
@@ -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<ReadOnlyBufferException> { 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())
}
}

0 comments on commit 9db9bd6

Please sign in to comment.