Skip to content

Commit 9db9bd6

Browse files
Read/write ByteString from/to ByteBuffer (#387)
* Read/write ByteString from/to ByteBuffer Closes #269 --------- Co-authored-by: Jake Wharton <jw@squareup.com>
1 parent 796a458 commit 9db9bd6

File tree

4 files changed

+296
-0
lines changed

4 files changed

+296
-0
lines changed

bytestring/api/kotlinx-io-bytestring.api

+6
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,14 @@ public final class kotlinx/io/bytestring/ByteStringBuilderKt {
6464
}
6565

6666
public final class kotlinx/io/bytestring/ByteStringJvmExtKt {
67+
public static final fun asReadOnlyByteBuffer (Lkotlinx/io/bytestring/ByteString;)Ljava/nio/ByteBuffer;
6768
public static final fun decodeToString (Lkotlinx/io/bytestring/ByteString;Ljava/nio/charset/Charset;)Ljava/lang/String;
6869
public static final fun encodeToByteString (Ljava/lang/String;Ljava/nio/charset/Charset;)Lkotlinx/io/bytestring/ByteString;
70+
public static final fun getByteString (Ljava/nio/ByteBuffer;I)Lkotlinx/io/bytestring/ByteString;
71+
public static final fun getByteString (Ljava/nio/ByteBuffer;II)Lkotlinx/io/bytestring/ByteString;
72+
public static synthetic fun getByteString$default (Ljava/nio/ByteBuffer;IILjava/lang/Object;)Lkotlinx/io/bytestring/ByteString;
73+
public static final fun putByteString (Ljava/nio/ByteBuffer;ILkotlinx/io/bytestring/ByteString;)V
74+
public static final fun putByteString (Ljava/nio/ByteBuffer;Lkotlinx/io/bytestring/ByteString;)V
6975
}
7076

7177
public final class kotlinx/io/bytestring/ByteStringKt {

bytestring/jvm/src/ByteStringJvmExt.kt

+114
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
package kotlinx.io.bytestring
77

8+
import kotlinx.io.bytestring.unsafe.UnsafeByteStringApi
9+
import kotlinx.io.bytestring.unsafe.UnsafeByteStringOperations
10+
import java.nio.BufferOverflowException
11+
import java.nio.ByteBuffer
812
import java.nio.charset.Charset
913

1014
/**
@@ -20,3 +24,113 @@ public fun ByteString.decodeToString(charset: Charset): String = getBackingArray
2024
* @param charset the encoding.
2125
*/
2226
public fun String.encodeToByteString(charset: Charset): ByteString = ByteString.wrap(toByteArray(charset))
27+
28+
/**
29+
* Returns a new read-only heap [ByteBuffer] wrapping [this] ByteString's content.
30+
*
31+
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.toReadOnlyByteBuffer
32+
*/
33+
@OptIn(UnsafeByteStringApi::class)
34+
public fun ByteString.asReadOnlyByteBuffer(): ByteBuffer {
35+
val data: ByteArray
36+
37+
UnsafeByteStringOperations.withByteArrayUnsafe(this) {
38+
data = it
39+
}
40+
41+
return ByteBuffer.wrap(data).asReadOnlyBuffer()
42+
}
43+
44+
/**
45+
* Reads [length] bytes of data from [this] ByteBuffer starting from the current position and
46+
* wraps them into a new [ByteString].
47+
*
48+
* Upon successful execution, current position will advance by [length].
49+
*
50+
* @throws IndexOutOfBoundsException when [length] has negative value or its value exceeds [ByteBuffer.remaining]
51+
*
52+
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.getByteStringFromBuffer
53+
*/
54+
@OptIn(UnsafeByteStringApi::class)
55+
public fun ByteBuffer.getByteString(length: Int = remaining()): ByteString {
56+
if (length < 0) {
57+
throw IndexOutOfBoundsException("length should be non-negative (was $length)")
58+
}
59+
if (remaining() < length) {
60+
throw IndexOutOfBoundsException("length ($length) exceeds remaining bytes count ({${remaining()}})")
61+
}
62+
val bytes = ByteArray(length)
63+
get(bytes)
64+
return UnsafeByteStringOperations.wrapUnsafe(bytes)
65+
}
66+
67+
/**
68+
* Reads [length] bytes of data from [this] ByteBuffer starting from [at] index and
69+
* wraps them into a new [ByteString].
70+
*
71+
* This function does not update [ByteBuffer.position].
72+
*
73+
* @throws IndexOutOfBoundsException when [at] is negative, greater or equal to [ByteBuffer.limit]
74+
* or [at] + [length] exceeds [ByteBuffer.limit].
75+
*
76+
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.getByteStringFromBufferAbsolute
77+
*/
78+
@OptIn(UnsafeByteStringApi::class)
79+
public fun ByteBuffer.getByteString(at: Int, length: Int): ByteString {
80+
checkIndexAndCapacity(at, length)
81+
val bytes = ByteArray(length)
82+
// Absolute get(byte[]) was added only in JDK 13
83+
for (i in 0..<length) {
84+
bytes[i] = get(at + i)
85+
}
86+
return UnsafeByteStringOperations.wrapUnsafe(bytes)
87+
}
88+
89+
/**
90+
* Writes [string] into [this] ByteBuffer starting from the current position.
91+
*
92+
* Upon successfully execution [ByteBuffer.position] will advance by the length of [string].
93+
*
94+
* @throws java.nio.ReadOnlyBufferException when [this] buffer is read-only
95+
* @throws java.nio.BufferOverflowException when [string] can't fit into remaining space of this buffer
96+
*
97+
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.putByteStringToBuffer
98+
*/
99+
@OptIn(UnsafeByteStringApi::class)
100+
public fun ByteBuffer.putByteString(string: ByteString) {
101+
UnsafeByteStringOperations.withByteArrayUnsafe(string) {
102+
put(it)
103+
}
104+
}
105+
106+
/**
107+
* Writes [string] into [this] ByteBuffer starting from position [at].
108+
*
109+
* This function does not update [ByteBuffer.position].
110+
*
111+
* @throws java.nio.ReadOnlyBufferException when [this] buffer is read-only
112+
* @throws IndexOutOfBoundsException when [at] is negative, exceeds [ByteBuffer.limit], or
113+
* [at] + [ByteString.size] exceeds [ByteBuffer.limit]
114+
*
115+
* @sample kotlinx.io.bytestring.samples.ByteStringSamplesJvm.putByteStringToBufferAbsolute
116+
*/
117+
public fun ByteBuffer.putByteString(at: Int, string: ByteString) {
118+
checkIndexAndCapacity(at, string.size)
119+
// Absolute put(byte[]) was added only in JDK 16
120+
for (idx in string.indices) {
121+
put(at + idx, string[idx])
122+
}
123+
}
124+
125+
private fun ByteBuffer.checkIndexAndCapacity(idx: Int, length: Int) {
126+
if (idx < 0 || idx >= limit()) {
127+
throw IndexOutOfBoundsException("Index $idx is out of this ByteBuffer's bounds: [0, ${limit()})")
128+
}
129+
if (length < 0) {
130+
throw IndexOutOfBoundsException("length should be non-negative (was $length)")
131+
}
132+
if (idx + length > limit()) {
133+
throw IndexOutOfBoundsException("There's not enough space to put ByteString of length $length starting" +
134+
" from index $idx")
135+
}
136+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. and respective authors and developers.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
4+
*/
5+
6+
package kotlinx.io.bytestring
7+
8+
import org.junit.jupiter.api.Test
9+
import java.nio.BufferOverflowException
10+
import java.nio.ByteBuffer
11+
import java.nio.ReadOnlyBufferException
12+
import kotlin.test.assertContentEquals
13+
import kotlin.test.assertEquals
14+
import kotlin.test.assertFailsWith
15+
import kotlin.test.assertTrue
16+
17+
public class ByteStringByteBufferExtensionsTest {
18+
@Test
19+
fun asReadOnlyByteBuffer() {
20+
val buffer = ByteString(1, 2, 3, 4).asReadOnlyByteBuffer()
21+
22+
assertTrue(buffer.isReadOnly)
23+
assertEquals(4, buffer.remaining())
24+
25+
ByteArray(4).let {
26+
buffer.get(it)
27+
assertContentEquals(byteArrayOf(1, 2, 3, 4), it)
28+
}
29+
}
30+
31+
@Test
32+
fun getByteString() {
33+
val bb = ByteBuffer.allocate(8)
34+
bb.put(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8))
35+
bb.flip()
36+
37+
assertEquals(ByteString(1, 2, 3, 4, 5, 6, 7, 8), bb.getByteString())
38+
bb.flip()
39+
40+
assertEquals(ByteString(1, 2, 3, 4), bb.getByteString(length = 4))
41+
assertEquals(ByteString(), bb.getByteString(length = 0))
42+
assertFailsWith<IndexOutOfBoundsException> { bb.getByteString(length = -1) }
43+
val p = bb.position()
44+
assertFailsWith<IndexOutOfBoundsException> { bb.getByteString(length = 5) }
45+
assertEquals(p, bb.position())
46+
bb.clear()
47+
48+
assertEquals(ByteString(1, 2, 3, 4, 5, 6, 7, 8), bb.getByteString(at = 0, length = 8))
49+
assertEquals(0, bb.position())
50+
51+
assertEquals(ByteString(2, 3, 4, 5), bb.getByteString(at = 1, length = 4))
52+
assertEquals(0, bb.position())
53+
54+
assertFailsWith<IndexOutOfBoundsException> { bb.getByteString(at = -1, length = 8) }
55+
assertFailsWith<IndexOutOfBoundsException> { bb.getByteString(at = 9, length = 1) }
56+
assertFailsWith<IndexOutOfBoundsException> { bb.getByteString(at = 7, length = 2) }
57+
assertFailsWith<IndexOutOfBoundsException> { bb.getByteString(at = 0, length = -1) }
58+
}
59+
60+
@Test
61+
fun putString() {
62+
val bb = ByteBuffer.allocate(8)
63+
val string = ByteString(1, 2, 3, 4, 5, 6, 7, 8)
64+
val shortString = ByteString(-1, -2, -3)
65+
66+
bb.putByteString(string)
67+
assertEquals(8, bb.position())
68+
bb.flip()
69+
ByteArray(8).let {
70+
bb.get(it)
71+
assertContentEquals(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8), it)
72+
}
73+
74+
bb.clear()
75+
bb.position(1)
76+
assertFailsWith<BufferOverflowException> { bb.putByteString(string) }
77+
assertEquals(1, bb.position())
78+
79+
bb.putByteString(at = 0, string = shortString)
80+
bb.putByteString(at = 5, string = shortString)
81+
assertEquals(1, bb.position())
82+
bb.clear()
83+
ByteArray(8).let {
84+
bb.get(it)
85+
assertContentEquals(byteArrayOf(-1, -2, -3, 4, 5, -1, -2, -3), it)
86+
}
87+
88+
assertFailsWith<IndexOutOfBoundsException> { bb.putByteString(at = 7, string = shortString) }
89+
assertFailsWith<IndexOutOfBoundsException> { bb.putByteString(at = -1, string = string) }
90+
assertFailsWith<IndexOutOfBoundsException> { bb.putByteString(at = 8, string = string) }
91+
assertFailsWith<ReadOnlyBufferException> {
92+
bb.asReadOnlyBuffer().putByteString(string)
93+
}
94+
assertFailsWith<ReadOnlyBufferException> {
95+
bb.asReadOnlyBuffer().putByteString(at = 0, string = string)
96+
}
97+
}
98+
}
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. and respective authors and developers.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENCE file.
4+
*/
5+
6+
package kotlinx.io.bytestring.samples
7+
8+
import kotlinx.io.bytestring.*
9+
import java.nio.ByteBuffer
10+
import java.nio.ReadOnlyBufferException
11+
import kotlin.test.*
12+
13+
public class ByteStringSamplesJvm {
14+
@Test
15+
fun toReadOnlyByteBuffer() {
16+
val str = "Hello World".encodeToByteString()
17+
val buffer = str.asReadOnlyByteBuffer()
18+
19+
assertEquals(11, buffer.remaining())
20+
assertEquals(0x48656c6c, buffer.getInt())
21+
22+
buffer.flip()
23+
assertFailsWith<ReadOnlyBufferException> { buffer.put(42) }
24+
}
25+
26+
@Test
27+
fun getByteStringFromBuffer() {
28+
val buffer = ByteBuffer.wrap("Hello World".encodeToByteArray())
29+
30+
// Consume the whole buffer
31+
val byteString = buffer.getByteString()
32+
assertEquals(0, buffer.remaining())
33+
assertEquals("Hello World".encodeToByteString(), byteString)
34+
35+
// Reset the buffer
36+
buffer.flip()
37+
// Consume only first 5 bytes from the buffer
38+
assertEquals("Hello".encodeToByteString(), buffer.getByteString(length = 5))
39+
}
40+
41+
@Test
42+
fun getByteStringFromBufferAbsolute() {
43+
val buffer = ByteBuffer.wrap("Hello World".encodeToByteArray())
44+
45+
// Read 2 bytes starting from offset 6
46+
val byteString = buffer.getByteString(at = 6, length = 2)
47+
// Buffer's position is not affected
48+
assertEquals(11, buffer.remaining())
49+
assertEquals(byteString, "Wo".encodeToByteString())
50+
}
51+
52+
@Test
53+
fun putByteStringToBuffer() {
54+
val buffer = ByteBuffer.allocate(32)
55+
val byteString = ByteString(0x66, 0xdb.toByte(), 0x11, 0x50)
56+
57+
// Putting a ByteString into a buffer will advance its position
58+
buffer.putByteString(byteString)
59+
assertEquals(4, buffer.position())
60+
61+
buffer.flip()
62+
assertEquals(1725632848, buffer.getInt())
63+
}
64+
65+
@Test
66+
fun putByteStringToBufferAbsolute() {
67+
val buffer = ByteBuffer.allocate(8)
68+
val byteString = ByteString(0x78, 0x5e)
69+
70+
// Putting a ByteString into a buffer using an absolute offset
71+
// won't change buffer's position.
72+
buffer.putByteString(at = 3, string = byteString)
73+
assertEquals(0, buffer.position())
74+
assertEquals(8, buffer.remaining())
75+
76+
assertEquals(0x000000785e000000L, buffer.getLong())
77+
}
78+
}

0 commit comments

Comments
 (0)