Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ buildscript {
}

plugins {
kotlin("jvm") version "1.4.31" apply false
id("org.jetbrains.dokka") version "1.4.20"
kotlin("jvm") version "1.5.0" apply false
id("org.jetbrains.dokka")
}

allprojects {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

package software.aws.clientrt.io

/**
* Exception thrown when a content-mutation method such as `write` is invoked upon a read-only buffer.
*/
class ReadOnlyBufferException : UnsupportedOperationException {
constructor() : super()

constructor(message: String?) : super(message)

constructor(message: String?, cause: Throwable?) : super(message, cause)

constructor(cause: Throwable?) : super(cause)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package software.aws.clientrt.io

import io.ktor.utils.io.bits.*
import io.ktor.utils.io.charsets.*
import io.ktor.utils.io.core.*
import software.aws.clientrt.util.InternalApi

Expand All @@ -15,6 +14,9 @@ private class SdkBufferState {
var readHead: Int = 0
}

@OptIn(ExperimentalIoApi::class)
internal expect fun Memory.Companion.ofByteArray(src: ByteArray, offset: Int = 0, length: Int = src.size - offset): Memory

/**
* A buffer with read and write positions. Similar in spirit to `java.nio.ByteBuffer` but for use
* in Kotlin Multiplatform.
Expand All @@ -27,13 +29,23 @@ private class SdkBufferState {
*/
@OptIn(ExperimentalIoApi::class)
@InternalApi
class SdkBuffer(initialCapacity: Int) {
class SdkBuffer internal constructor(
// we make use of ktor-io's `Memory` type which already implements most of the functionality in a platform
// agnostic way. We just need to wrap some methods around it
internal var memory: Memory,
val isReadOnly: Boolean = false
) {
constructor(initialCapacity: Int, readOnly: Boolean = false) : this(DefaultAllocator.alloc(initialCapacity), readOnly)

// TODO - we could implement Appendable but we would need to deal with Char as UTF-16 character
// (e.g. convert code points to number of bytes and write the correct utf bytes 1..4)

// we make use of ktor-io's `Memory` type which already implements most of the functionality in a platform
// agnostic way. We just need to wrap some methods around it
internal var memory = DefaultAllocator.alloc(initialCapacity)
companion object {
/**
* Create an SdkBuffer backed by the given ByteArray
*/
fun of(src: ByteArray, offset: Int = 0, length: Int = src.size - offset): SdkBuffer = SdkBuffer(Memory.ofByteArray(src, offset, length))
}

private val state = SdkBufferState()

Expand Down Expand Up @@ -75,8 +87,8 @@ class SdkBuffer(initialCapacity: Int) {
fun reserve(count: Int) {
if (writeRemaining >= count) return

val minP2 = ceilp2(count)
val currP2 = ceilp2(memory.size32 + 1)
val minP2 = ceilp2(count + writePosition)
val currP2 = ceilp2(memory.size32 + writePosition + 1)
val newSize = maxOf(minP2, currP2)
memory = DefaultAllocator.realloc(memory, newSize)
}
Expand Down Expand Up @@ -125,6 +137,12 @@ class SdkBuffer(initialCapacity: Int) {
public inline val SdkBuffer.canRead: Boolean
get() = writePosition > readPosition

/**
* Creates a new, read-only byte buffer that shares this buffer's content.
* Any attempts to write to the buffer will fail with [ReadOnlyBufferException]
*/
fun SdkBuffer.asReadOnly(): SdkBuffer = if (isReadOnly) this else SdkBuffer(memory, isReadOnly = true)

/**
* Read from this buffer exactly [length] bytes and write to [dest] starting at [offset]
* @throws IllegalArgumentException if there are not enough bytes available for read or the offset/length combination is invalid
Expand Down Expand Up @@ -237,6 +255,8 @@ private inline fun SdkBuffer.read(block: (memory: Memory, readStart: Int, endExc

@OptIn(ExperimentalIoApi::class)
private inline fun SdkBuffer.write(block: (memory: Memory, writeStart: Int, endExclusive: Int) -> Int): Int {
if (isReadOnly) throw ReadOnlyBufferException("attempt to write to readOnly buffer at index: $writePosition")

val wc = block(memory, writePosition, capacity)
commitWritten(wc)
return wc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

package software.aws.clientrt.io

import io.ktor.utils.io.core.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue

class SdkBufferTest {
@Test
Expand Down Expand Up @@ -206,6 +208,15 @@ class SdkBufferTest {
assertEquals(128, buf.capacity)
}

@Test
fun testReserveExistingData() {
// https://github.com/awslabs/aws-sdk-kotlin/issues/147
val buf = SdkBuffer(256)
buf.commitWritten(138)
buf.reserve(444)
assertEquals(1024, buf.capacity)
}

@Test
fun testWriteFullyPastDestSize() {
val buf = SdkBuffer(16)
Expand Down Expand Up @@ -281,4 +292,45 @@ class SdkBufferTest {
val bytes = buf.bytes()
assertEquals(16, bytes.size)
}

@Test
fun testReadOnly() {
val buf = SdkBuffer(16, readOnly = true)
val data = "foo"

assertFailsWith<ReadOnlyBufferException> {
buf.write(data)
}
assertFailsWith<ReadOnlyBufferException> {
buf.writeFully(data.encodeToByteArray())
}
assertFailsWith<ReadOnlyBufferException> {
val src = SdkBuffer.of(data.encodeToByteArray())
buf.writeFully(src)
}

assertTrue(buf.isReadOnly)
val buf2 = SdkBuffer(16).asReadOnly()
assertTrue(buf2.isReadOnly)
}

@Test
fun testOfByteArray() {
val data = "hello world".toByteArray()
val buf = SdkBuffer.of(data)
assertEquals(data.size, buf.capacity)

// does not automatically make the contents readable
assertEquals(0, buf.readRemaining)
buf.commitWritten(data.size)
assertEquals(data.size, buf.readRemaining)

buf.reset()
buf.commitWritten(6)
buf.write("tests")
assertEquals("hello tests", buf.decodeToString())

// original buffer should have been modified
assertEquals("hello tests", data.decodeToString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@

package software.aws.clientrt.io

import io.ktor.utils.io.bits.*
import java.nio.ByteBuffer

internal fun SdkBuffer.hasArray() = memory.buffer.hasArray() && !memory.buffer.isReadOnly

actual fun SdkBuffer.bytes(): ByteArray = when (hasArray()) {
true -> memory.buffer.array().sliceArray(readPosition until readRemaining)
false -> ByteArray(readRemaining).apply { readFully(this) }
}

internal actual fun Memory.Companion.ofByteArray(src: ByteArray, offset: Int, length: Int): Memory =
Memory.of(src, offset, length)
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why is this platform-dependent? Couldn't this be implemented in common instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd think so but it does not exist in ktor-io common. I'm not sure why but I suspect some limitation of K/N as it exists today. But I agree which is why I defined the extension in common and figure it will resolve itself or we'll deal with it when the time comes


/**
* Create a new SdkBuffer using the given [ByteBuffer] as the contents
*/
fun SdkBuffer.Companion.of(byteBuffer: ByteBuffer): SdkBuffer = SdkBuffer(Memory.of(byteBuffer))
5 changes: 5 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ pluginManagement {
google()
gradlePluginPortal()
}

// configure the smithy-gradle plugin version
plugins {
id("org.jetbrains.dokka") version "1.4.32"
}
}

rootProject.name = "smithy-kotlin"
Expand Down