Skip to content

Commit 613f51b

Browse files
committed
Read/write ByteString from/to ByteBuffer
Closes #269
1 parent 92e0214 commit 613f51b

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

bytestring/api/kotlinx-io-bytestring.api

Lines changed: 6 additions & 0 deletions
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

Lines changed: 104 additions & 0 deletions
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,103 @@ 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+
@OptIn(UnsafeByteStringApi::class)
32+
public fun ByteString.asReadOnlyByteBuffer(): ByteBuffer {
33+
val data: ByteArray
34+
35+
UnsafeByteStringOperations.withByteArrayUnsafe(this) {
36+
data = it
37+
}
38+
39+
return ByteBuffer.wrap(data).asReadOnlyBuffer()
40+
}
41+
42+
/**
43+
* Reads [length] bytes of data from [this] ByteBuffer starting from the current position and
44+
* wraps them into a new [ByteString].
45+
*
46+
* Upon successful execution, current position will advance by [length].
47+
*
48+
* @throws IndexOutOfBoundsException when [length] has negative value or its value exceeds [ByteBuffer.remaining]
49+
*/
50+
@OptIn(UnsafeByteStringApi::class)
51+
public fun ByteBuffer.getByteString(length: Int = remaining()): ByteString {
52+
if (length < 0) {
53+
throw IndexOutOfBoundsException("length should be non-negative (was $length)")
54+
}
55+
if (remaining() < length) {
56+
throw IndexOutOfBoundsException("length ($length) exceeds remaining bytes count ({${remaining()}})")
57+
}
58+
val bytes = ByteArray(length)
59+
get(bytes)
60+
return UnsafeByteStringOperations.wrapUnsafe(bytes)
61+
}
62+
63+
/**
64+
* Reads [length] bytes of data from [this] ByteBuffer starting from [at] index and
65+
* wraps them into a new [ByteString].
66+
*
67+
* This function does not update [ByteBuffer.position].
68+
*
69+
* @throws IndexOutOfBoundsException when [at] is negative, greater or equal to [ByteBuffer.limit]
70+
* or [at] + [length] exceeds [ByteBuffer.limit].
71+
*/
72+
@OptIn(UnsafeByteStringApi::class)
73+
public fun ByteBuffer.getByteString(at: Int, length: Int): ByteString {
74+
checkIndexAndCapacity(at, length)
75+
val bytes = ByteArray(length)
76+
// Absolute get(byte[]) was added only in JDK 13
77+
for (i in 0..<length) {
78+
bytes[i] = get(at + i)
79+
}
80+
return UnsafeByteStringOperations.wrapUnsafe(bytes)
81+
}
82+
83+
/**
84+
* Writes [string] into [this] ByteBuffer starting from the current position.
85+
*
86+
* Upon successfully execution [ByteBuffer.position] will advance by the length of [string].
87+
*
88+
* @throws java.nio.ReadOnlyBufferException when [this] buffer is read-only
89+
* @throws java.nio.BufferOverflowException when [string] can't fit into remaining space of this buffer
90+
*/
91+
@OptIn(UnsafeByteStringApi::class)
92+
public fun ByteBuffer.putByteString(string: ByteString) {
93+
UnsafeByteStringOperations.withByteArrayUnsafe(string) {
94+
put(it)
95+
}
96+
}
97+
98+
/**
99+
* Writes [string] into [this] ByteBuffer starting from position [at].
100+
*
101+
* This function does not update [ByteBuffer.position].
102+
*
103+
* @throws java.nio.ReadOnlyBufferException when [this] buffer is read-only
104+
* @throws IndexOutOfBoundsException when [at] is negative, exceeds [ByteBuffer.limit], or
105+
* [at] + [ByteString.size] exceeds [ByteBuffer.limit]
106+
*/
107+
public fun ByteBuffer.putByteString(at: Int, string: ByteString) {
108+
checkIndexAndCapacity(at, string.size)
109+
// Absolute get(byte[]) was added only in JDK 13
110+
for (idx in string.indices) {
111+
put(at + idx, string[idx])
112+
}
113+
}
114+
115+
private fun ByteBuffer.checkIndexAndCapacity(idx: Int, length: Int) {
116+
if (idx < 0 || idx >= limit()) {
117+
throw IndexOutOfBoundsException("Index $idx is out of this ByteBuffer's bounds: [0, ${limit()})")
118+
}
119+
if (length < 0) {
120+
throw IndexOutOfBoundsException("length should be non-negative (was $length)")
121+
}
122+
if (idx + length > limit()) {
123+
throw IndexOutOfBoundsException("There's not enough space to put ByteString of length $length starting" +
124+
" from index $idx")
125+
}
126+
}
Lines changed: 98 additions & 0 deletions
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 ByteStringByteBufferExtensions {
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+
}

0 commit comments

Comments
 (0)