From a4734c7551694dd90a71aa5d1e4818ec9ae7b47f Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:30:46 -0500 Subject: [PATCH 01/58] fix: create JMESPATH flattenIfPossible functions for Lists (#1169) --- .../smithy/kotlin/runtime/util/JMESPath.kt | 10 ++++ .../kotlin/runtime/util/JmesPathTest.kt | 49 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt index 2a7cdd9791..9b691a79a2 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt @@ -66,6 +66,7 @@ public fun Any?.type(): String = when (this) { else -> throw Exception("Undetected type for: $this") } +// Collection `flattenIfPossible` functions @InternalApi @JvmName("noOpUnnestedCollection") public inline fun Collection.flattenIfPossible(): Collection = this @@ -73,3 +74,12 @@ public inline fun Collection.flattenIfPossible(): Collection = @InternalApi @JvmName("flattenNestedCollection") public inline fun Collection>.flattenIfPossible(): Collection = flatten() + +// List `flattenIfPossible` functions +@InternalApi +@JvmName("noOpUnnestedCollection") +public inline fun List.flattenIfPossible(): List = this + +@InternalApi +@JvmName("flattenNestedCollection") +public inline fun List>.flattenIfPossible(): List = flatten() diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt new file mode 100644 index 0000000000..e867f1e8a1 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt @@ -0,0 +1,49 @@ +package aws.smithy.kotlin.runtime.util + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class JmesPathTest { + @Test + fun flattenNestedLists() { + val nestedList = listOf( + listOf(1, 2, 3), + listOf(4, 5), + listOf(6), + ) + val flattenedList = nestedList.flattenIfPossible() + assertEquals(listOf(1, 2, 3, 4, 5, 6), flattenedList) + } + + @Test + fun flattenEmptyNestedLists() { + val nestedList = listOf( + listOf(), + listOf(), + listOf(), + ) + val flattenedList = nestedList.flattenIfPossible() + assertTrue(flattenedList.isEmpty()) + } + + @Test + fun flattenNestedEmptyAndNonEmptyNestedLists() { + val nestedList = listOf( + listOf(1, 2), + listOf(), + listOf(3, 4, 5), + ) + val flattenedList = nestedList.flattenIfPossible() + assertEquals(listOf(1, 2, 3, 4, 5), flattenedList) + } + + @Test + fun flattenList() { + val nestedList = listOf( + listOf(1, 2, 3), + ) + val flattenedList = nestedList.flattenIfPossible() + assertEquals(listOf(1, 2, 3), flattenedList) + } +} From 7911a94dd5f6ee27f2652aeb7c8031e2dce79215 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 16 Dec 2024 16:51:49 +0000 Subject: [PATCH 02/58] chore: release 1.3.30 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 892e2b1248..5633a30d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.3.30] - 12/16/2024 + ## [1.3.29] - 12/12/2024 ## [1.3.28] - 12/03/2024 diff --git a/gradle.properties b/gradle.properties index f35cfc1d5a..377ff6666b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.30-SNAPSHOT +sdkVersion=1.3.30 # codegen -codegenVersion=0.33.30-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.30 \ No newline at end of file From 7085c8ab0d4b34d3ddb77bbb31a2fd41c47c288c Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 16 Dec 2024 16:51:50 +0000 Subject: [PATCH 03/58] chore: bump snapshot version to 1.3.31-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 377ff6666b..3628520e5a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.30 +sdkVersion=1.3.31-SNAPSHOT # codegen -codegenVersion=0.33.30 \ No newline at end of file +codegenVersion=0.33.31-SNAPSHOT \ No newline at end of file From 45750232626bbe4e279142e486be8bbbfccab33f Mon Sep 17 00:00:00 2001 From: Matas Date: Wed, 18 Dec 2024 14:20:08 -0500 Subject: [PATCH 04/58] misc: enhance support for replayable instances of `InputStream` (#1197) --- .../2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json | 8 ++++++ .../kotlin/runtime/content/ByteStreamJVM.kt | 23 +++++++++++++-- .../runtime/content/ByteStreamJVMTest.kt | 28 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json diff --git a/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json b/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json new file mode 100644 index 0000000000..fb16fa5843 --- /dev/null +++ b/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json @@ -0,0 +1,8 @@ +{ + "id": "2de8162f-e5a0-4618-b00d-8da3cfdbc6a2", + "type": "feature", + "description": "Enhance support for replayable instances of `InputStream`", + "issues": [ + "https://github.com/awslabs/aws-sdk-kotlin/issues/1473" + ] +} \ No newline at end of file diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt index 1d6124357a..5647ac15af 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt @@ -114,11 +114,30 @@ public fun ByteStream.Companion.fromInputStream( * @param contentLength If specified, indicates how many bytes remain in this stream. Defaults to `null`. */ public fun InputStream.asByteStream(contentLength: Long? = null): ByteStream.SourceStream { - val source = source() + if (markSupported() && contentLength != null) { + mark(contentLength.toInt()) + } + return object : ByteStream.SourceStream() { override val contentLength: Long? = contentLength override val isOneShot: Boolean = !markSupported() - override fun readFrom(): SdkSource = source + override fun readFrom(): SdkSource { + if (markSupported() && contentLength != null) { + reset() + mark(contentLength.toInt()) + return object : SdkSource by source() { + /* + * This is a no-op close to prevent body hashing from closing the underlying InputStream, which causes + * `IOException: Stream closed` on subsequent reads. Consider making [ByteStream.ChannelStream]/[ByteStream.SourceStream] + * (or possibly even [ByteStream] itself) implement [Closeable] to better handle closing streams. + * This should allow us to clean up our usage of [ByteStream.cancel()]. + */ + override fun close() { } + } + } + + return source() + } } } diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt index 054387b2e5..e8324fb11f 100644 --- a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt +++ b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt @@ -5,8 +5,11 @@ package aws.smithy.kotlin.runtime.content +import aws.smithy.kotlin.runtime.io.readToByteArray import aws.smithy.kotlin.runtime.testing.RandomTempFile import kotlinx.coroutines.test.runTest +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream import java.io.OutputStream @@ -228,6 +231,31 @@ class ByteStreamJVMTest { assertFalse(sos.closed) } + // https://github.com/awslabs/aws-sdk-kotlin/issues/1473 + @Test + fun testReplayableInputStreamAsByteStream() = runTest { + val content = "Hello, Bytes!".encodeToByteArray() + val byteArrayIns = ByteArrayInputStream(content) + val nonReplayableIns = NonReplayableInputStream(byteArrayIns) + + // buffer the non-replayable stream, making it replayable... + val bufferedIns = BufferedInputStream(nonReplayableIns) + + val byteStream = bufferedIns.asByteStream(content.size.toLong()) + + // Test that it can be read at least twice (e.g. once for hashing the body, once for transmitting the body) + assertContentEquals(content, byteStream.readFrom().use { it.readToByteArray() }) + assertContentEquals(content, byteStream.readFrom().use { it.readToByteArray() }) + } + + private class NonReplayableInputStream(val inputStream: InputStream) : InputStream() { + override fun markSupported(): Boolean = false // not replayable + + override fun read(): Int = inputStream.read() + override fun mark(readlimit: Int) = inputStream.mark(readlimit) + override fun reset() = inputStream.reset() + } + private class StatusTrackingOutputStream(val os: OutputStream) : OutputStream() { var closed: Boolean = false From b0a4bacc3626bf2c9e8cfe9607ef8d5ece03ee4e Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 18 Dec 2024 19:23:06 +0000 Subject: [PATCH 05/58] chore: release 1.3.31 --- .changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json | 8 -------- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 .changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json diff --git a/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json b/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json deleted file mode 100644 index fb16fa5843..0000000000 --- a/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "2de8162f-e5a0-4618-b00d-8da3cfdbc6a2", - "type": "feature", - "description": "Enhance support for replayable instances of `InputStream`", - "issues": [ - "https://github.com/awslabs/aws-sdk-kotlin/issues/1473" - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5633a30d73..5473a56d69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.3.31] - 12/18/2024 + +### Features +* [#1473](https://github.com/awslabs/aws-sdk-kotlin/issues/1473) Enhance support for replayable instances of `InputStream` + ## [1.3.30] - 12/16/2024 ## [1.3.29] - 12/12/2024 diff --git a/gradle.properties b/gradle.properties index 3628520e5a..cf967edd0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.31-SNAPSHOT +sdkVersion=1.3.31 # codegen -codegenVersion=0.33.31-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.31 \ No newline at end of file From 80f453814d8b0c4e20ef7e729d47a364ca8b5753 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 18 Dec 2024 19:23:08 +0000 Subject: [PATCH 06/58] chore: bump snapshot version to 1.3.32-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index cf967edd0b..f902a36719 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.31 +sdkVersion=1.3.32-SNAPSHOT # codegen -codegenVersion=0.33.31 \ No newline at end of file +codegenVersion=0.33.32-SNAPSHOT \ No newline at end of file From e9d16a98b8030468e35a05f63c79cae2444e6759 Mon Sep 17 00:00:00 2001 From: Matas Date: Wed, 18 Dec 2024 16:28:30 -0500 Subject: [PATCH 07/58] fix: CBOR protocol test assertions / blob serialization (#1198) --- .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json | 5 +++++ .../protocol/HttpProtocolUnitTestRequestGenerator.kt | 4 ++-- .../codegen/rendering/serde/SerializeStructGenerator.kt | 1 - .../codegen/rendering/serde/SerializeStructGeneratorTest.kt | 2 +- .../src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json diff --git a/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json b/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json new file mode 100644 index 0000000000..0fad5e0349 --- /dev/null +++ b/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json @@ -0,0 +1,5 @@ +{ + "id": "1021e75a-45f3-4f3a-820c-700d9ec6e782", + "type": "bugfix", + "description": "Fix serialization of CBOR blobs" +} \ No newline at end of file diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt index 74bd7db782..6fcc47c3e5 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt @@ -117,11 +117,11 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B write("return") } write("requireNotNull(expectedBytes) { #S }", "expected application/cbor body cannot be null") - write("requireNotNull(expectedBytes) { #S }", "actual application/cbor body cannot be null") + write("requireNotNull(actualBytes) { #S }", "actual application/cbor body cannot be null") write("") write("val expectedRequest = #L(#T(expectedBytes))", inputDeserializer.name, RuntimeTypes.Serde.SerdeCbor.CborDeserializer) - write("val actualRequest = #L(#T(expectedBytes))", inputDeserializer.name, RuntimeTypes.Serde.SerdeCbor.CborDeserializer) + write("val actualRequest = #L(#T(actualBytes))", inputDeserializer.name, RuntimeTypes.Serde.SerdeCbor.CborDeserializer) write("assertEquals(expectedRequest, actualRequest)") } writer.write("") diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt index e7f2e261ef..d8810044a5 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt @@ -647,7 +647,6 @@ open class SerializeStructGenerator( val target = member.targetOrSelf(ctx.model) val encoded = when { - target.type == ShapeType.BLOB -> writer.format("#L.#T()", identifier, RuntimeTypes.Core.Text.Encoding.encodeBase64String) target.type == ShapeType.TIMESTAMP -> { writer.addImport(RuntimeTypes.Core.TimestampFormat) val tsFormat = member diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt index 40d9c9db7d..347da007cb 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt @@ -1822,7 +1822,7 @@ class SerializeStructGeneratorTest { val expected = """ serializer.serializeStruct(OBJ_DESCRIPTOR) { - input.fooBlob?.let { field(FOOBLOB_DESCRIPTOR, it.encodeBase64String()) } + input.fooBlob?.let { field(FOOBLOB_DESCRIPTOR, it) } } """.trimIndent() diff --git a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt b/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt index 35618cb49e..d1356f848c 100644 --- a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt +++ b/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt @@ -134,7 +134,7 @@ public class XmlSerializer(private val xmlWriter: XmlStreamWriter = xmlStreamWri field(descriptor, value.format(format)) override fun field(descriptor: SdkFieldDescriptor, value: ByteArray): Unit = - field(descriptor, value) + field(descriptor, value.encodeBase64String()) override fun field(descriptor: SdkFieldDescriptor, value: Document?): Unit = throw SerializationException( "cannot serialize field ${descriptor.serialName}; Document type is not supported by xml encoding", From 48849a1208fe0bb2c42c06c49b54b9c419104378 Mon Sep 17 00:00:00 2001 From: Matas Date: Mon, 6 Jan 2025 16:34:11 -0500 Subject: [PATCH 08/58] fix: correctly serialize subset of shape's members when configured (#1199) --- .../core/QueryHttpBindingProtocolGenerator.kt | 2 +- .../codegen/aws/protocols/RpcV2CborTest.kt | 17 +++++++++++++++++ .../rendering/serde/CborSerializerGenerator.kt | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt index 62f8f7ebe1..db97349f84 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt @@ -145,7 +145,7 @@ abstract class AbstractQueryFormUrlSerializerGenerator( return shape.documentSerializer(ctx.settings, symbol, members) { writer -> writer.openBlock("internal fun #identifier.name:L(serializer: #T, input: #T) {", RuntimeTypes.Serde.Serializer, symbol) .call { - renderSerializerBody(ctx, shape, shape.members().toList(), writer) + renderSerializerBody(ctx, shape, members.toList(), writer) } .closeBlock("}") } diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt index 17a7c0f3d1..0114185004 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt @@ -4,6 +4,7 @@ */ package software.amazon.smithy.kotlin.codegen.aws.protocols +import io.kotest.matchers.string.shouldNotContain import software.amazon.smithy.kotlin.codegen.test.* import kotlin.test.Test @@ -145,4 +146,20 @@ class RpcV2CborTest { val serializeBody = serializer.lines(" override suspend fun serialize(context: ExecutionContext, input: PutFooStreamingRequest): HttpRequestBuilder {", "}") serializeBody.shouldContainOnlyOnceWithDiff("""builder.headers.setMissing("Content-Type", "application/vnd.amazon.eventstream")""") } + + @Test + fun testEventStreamInitialRequestDoesNotSerializeStreamMember() { + val ctx = model.newTestContext("CborExample") + + val generator = RpcV2Cbor() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val documentSerializer = ctx.manifest.expectFileString("/src/main/kotlin/com/test/serde/PutFooStreamingRequestDocumentSerializer.kt") + + val serializeBody = documentSerializer.lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", "}") + serializeBody.shouldNotContain("input.messages") // `messages` is the stream member and should not be serialized in the initial request + } } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt index 1fdb7850a7..545a484a90 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt @@ -108,7 +108,7 @@ class CborSerializerGenerator( val symbol = ctx.symbolProvider.toSymbol(shape) return shape.documentSerializer(ctx.settings, symbol, members) { writer -> writer.withBlock("internal fun #identifier.name:L(serializer: #T, input: #T) {", "}", RuntimeTypes.Serde.Serializer, symbol) { - call { renderSerializerBody(ctx, shape, shape.members().toList(), writer) } + call { renderSerializerBody(ctx, shape, members.toList(), writer) } } } } From ded3a4bb03060f28f3ce085fed3a9627f7297ae5 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 6 Jan 2025 21:45:24 +0000 Subject: [PATCH 09/58] chore: release 1.3.32 --- .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json diff --git a/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json b/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json deleted file mode 100644 index 0fad5e0349..0000000000 --- a/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "1021e75a-45f3-4f3a-820c-700d9ec6e782", - "type": "bugfix", - "description": "Fix serialization of CBOR blobs" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5473a56d69..a52fe756c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.3.32] - 01/06/2025 + +### Fixes +* Fix serialization of CBOR blobs + ## [1.3.31] - 12/18/2024 ### Features diff --git a/gradle.properties b/gradle.properties index f902a36719..eda623b285 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.32-SNAPSHOT +sdkVersion=1.3.32 # codegen -codegenVersion=0.33.32-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.32 \ No newline at end of file From d97e8ba056d5f0c9869226854638dd486e06f1fa Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 6 Jan 2025 21:45:25 +0000 Subject: [PATCH 10/58] chore: bump snapshot version to 1.3.33-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index eda623b285..b49ffff27f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.32 +sdkVersion=1.3.33-SNAPSHOT # codegen -codegenVersion=0.33.32 \ No newline at end of file +codegenVersion=0.33.33-SNAPSHOT \ No newline at end of file From 49f464b7a915efc5aa78445a94ea4eb2e4f02577 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 7 Jan 2025 13:19:25 -0500 Subject: [PATCH 11/58] Initial GZIP implementation --- .../kotlin/runtime/compression/GzipTest.kt | 42 +++++++++ .../kotlin/runtime/compression/GzipNative.kt | 92 +++++++++++++++++-- .../runtime/io/GzipByteReadChannelNative.kt | 33 +++++-- .../kotlin/runtime/io/GzipSdkSourceNative.kt | 20 +++- .../kotlin/runtime/io/GzipTestUtilsNative.kt | 75 ++++++++++++++- 5 files changed, 241 insertions(+), 21 deletions(-) create mode 100644 runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt new file mode 100644 index 0000000000..14773ddc46 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.compression + +import aws.smithy.kotlin.runtime.content.ByteStream +import aws.smithy.kotlin.runtime.content.toByteArray +import aws.smithy.kotlin.runtime.io.decompressGzipBytes +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContentEquals + +class GzipTest { + @Test + fun testCompress() = runTest { + val payload = "Hello World".encodeToByteArray() + val byteStream = ByteStream.fromBytes(payload) + + val compressed = Gzip() + .compress(byteStream) + .toByteArray() + + val decompressedBytes = decompressGzipBytes(compressed) + assertContentEquals(payload, decompressedBytes) + } + + @Test + fun testCompressEmptyByteArray() = runTest { + val payload = ByteArray(0) + val byteStream = ByteStream.fromBytes(payload) + + val compressed = Gzip() + .compress(byteStream) + .toByteArray() + + val decompressedBytes = decompressGzipBytes(compressed) + + assertContentEquals(payload, decompressedBytes) + } +} \ No newline at end of file diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index a9fc036399..d1bbfac4a1 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -5,6 +5,19 @@ package aws.smithy.kotlin.runtime.compression import aws.smithy.kotlin.runtime.content.ByteStream +import aws.smithy.kotlin.runtime.io.GzipByteReadChannel +import aws.smithy.kotlin.runtime.io.GzipSdkSource +import aws.smithy.kotlin.runtime.io.SdkByteReadChannel +import aws.smithy.kotlin.runtime.io.SdkSource +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.refTo +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.sizeOf +import kotlinx.cinterop.usePinned +import platform.zlib.* /** * The gzip compression algorithm. @@ -13,12 +26,79 @@ import aws.smithy.kotlin.runtime.content.ByteStream * See: https://en.wikipedia.org/wiki/Gzip */ public actual class Gzip : CompressionAlgorithm { - actual override val id: String - get() = TODO("Not yet implemented") - actual override val contentEncoding: String - get() = TODO("Not yet implemented") + actual override val id: String = "gzip" + actual override val contentEncoding: String = "gzip" - actual override fun compress(stream: ByteStream): ByteStream { - TODO("Not yet implemented") + actual override fun compress(stream: ByteStream): ByteStream = when (stream) { + is ByteStream.ChannelStream -> object : ByteStream.ChannelStream() { + override fun readFrom(): SdkByteReadChannel = GzipByteReadChannel(stream.readFrom()) + override val contentLength: Long? = null + override val isOneShot: Boolean = stream.isOneShot + } + + is ByteStream.SourceStream -> object : ByteStream.SourceStream() { + override fun readFrom(): SdkSource = GzipSdkSource(stream.readFrom()) + override val contentLength: Long? = null + override val isOneShot: Boolean = stream.isOneShot + } + + is ByteStream.Buffer -> { + val sourceBytes = stream.bytes() + if (sourceBytes.isEmpty()) { + stream + } else { + val compressed = gzipCompressBytes(sourceBytes) + + object : ByteStream.Buffer() { + override fun bytes(): ByteArray = compressed + } + } + } + } +} + +internal fun gzipCompressBytes(bytes: ByteArray): ByteArray { + val compressedBuffer = UByteArray(bytes.size + 128) + memScoped { + val zStream = alloc().apply { + zalloc = null + zfree = null + opaque = null + } + + // Initialize the deflate context with gzip encoding + val initResult = deflateInit2_( + strm = zStream.ptr, + level = Z_BEST_COMPRESSION, + method = Z_DEFLATED, + windowBits = 31, // 15 (max window bits) + 16 (GZIP offset) + memLevel = 9, + strategy = Z_DEFAULT_STRATEGY, + version = ZLIB_VERSION, + stream_size = sizeOf().toInt() + ) + if (initResult != Z_OK) { + throw IllegalStateException("Failed to initialize zlib with error code $initResult") + } + + try { + zStream.next_in = bytes.refTo(0).getPointer(memScope).reinterpret() + zStream.avail_in = bytes.size.toUInt() + + compressedBuffer.usePinned { pinnedBuffer -> + zStream.next_out = pinnedBuffer.addressOf(0) + zStream.avail_out = compressedBuffer.size.toUInt() + + val deflateResult = deflate(zStream.ptr, Z_FINISH) + if (deflateResult != Z_STREAM_END) { + throw IllegalStateException("Compression failed with error code $deflateResult") + } + } + + val compressedSize = compressedBuffer.size.toUInt() - zStream.avail_out + return compressedBuffer.copyOf(compressedSize.toInt()).toByteArray() + } finally { + deflateEnd(zStream.ptr) + } } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt index f8ca725e14..b55b265030 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt @@ -5,26 +5,39 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.compression.gzipCompressBytes /** - * Wraps the SdkByteReadChannel so that it compresses into gzip format with each read. + * Wraps an [SdkByteReadChannel], compressing bytes read into GZIP format. + * @param channel the [SdkByteReadChannel] to compress the contents of */ @InternalApi -public actual class GzipByteReadChannel actual constructor(channel: SdkByteReadChannel) : SdkByteReadChannel { +public actual class GzipByteReadChannel actual constructor(public val channel: SdkByteReadChannel) : SdkByteReadChannel { actual override val availableForRead: Int - get() = TODO("Not yet implemented") + get() = channel.availableForRead + actual override val isClosedForRead: Boolean - get() = TODO("Not yet implemented") + get() = channel.isClosedForRead + actual override val isClosedForWrite: Boolean - get() = TODO("Not yet implemented") + get() = channel.isClosedForWrite + actual override val closedCause: Throwable? - get() = TODO("Not yet implemented") + get() = channel.closedCause actual override suspend fun read(sink: SdkBuffer, limit: Long): Long { - TODO("Not yet implemented") - } + require(limit >= 0L) + if (limit == 0L) return 0L - actual override fun cancel(cause: Throwable?): Boolean { - TODO("Not yet implemented") + val temp = SdkBuffer() + val rc = channel.read(temp, limit) + + if (rc <= 0) { return rc } + + val compressed = gzipCompressBytes(temp.readByteArray()) + sink.write(compressed) + return compressed.size.toLong() } + + actual override fun cancel(cause: Throwable?): Boolean = channel.cancel(cause) } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt index f38cfd45d1..e67ff371c2 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt @@ -5,17 +5,29 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.compression.gzipCompressBytes /** - * Wraps the SdkSource so that it compresses into gzip format with each read. + * Wraps an [SdkSource], compressing bytes read into GZIP format. + * @param source the [SdkSource] to compress the contents of */ @InternalApi -public actual class GzipSdkSource actual constructor(source: SdkSource) : SdkSource { +public actual class GzipSdkSource actual constructor(public val source: SdkSource) : SdkSource { actual override fun read(sink: SdkBuffer, limit: Long): Long { - TODO("Not yet implemented") + require(limit >= 0L) + if (limit == 0L) return 0L + + val temp = SdkBuffer() + val rc = source.read(temp, limit) + + if (rc <= 0L) { return rc } + + val compressed = gzipCompressBytes(temp.readByteArray()) + sink.write(compressed) + return compressed.size.toLong() } actual override fun close() { - TODO("Not yet implemented") + source.close() } } diff --git a/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt b/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt index 198808f770..7849bd63af 100644 --- a/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt +++ b/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt @@ -4,9 +4,82 @@ */ package aws.smithy.kotlin.runtime.io +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.refTo +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.sizeOf +import kotlinx.cinterop.usePinned +import platform.zlib.* + /** * Decompresses a byte array compressed using the gzip format */ internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { - TODO("Not yet implemented") + if (bytes.isEmpty()) { return bytes } + + val decompressedBuffer = UByteArray(bytes.size * 2) // Initial guess for decompressed size (may expand) + + memScoped { + val zStream = alloc().apply { + zalloc = null + zfree = null + opaque = null + } + + println("inflateInit2_") + // Initialize the inflate context for gzip decoding + val result = inflateInit2_( + strm = zStream.ptr, + windowBits = 31, + version = ZLIB_VERSION, + stream_size = sizeOf().toInt() + ) + if (result != Z_OK) { throw IllegalStateException("inflateInit2_ failed with error code $result") } + + try { + zStream.next_in = bytes.refTo(0).getPointer(memScope).reinterpret() + zStream.avail_in = bytes.size.toUInt() + println("zStream.next_in = ${zStream.next_in}, zStream.avail_in = ${zStream.avail_in}") + + val output = mutableListOf() + while (zStream.avail_in > 0u) { + println("loop zStream.avail_in = ${zStream.avail_in}") + + decompressedBuffer.usePinned { pinnedBuffer -> + zStream.next_out = pinnedBuffer.addressOf(0) + zStream.avail_out = decompressedBuffer.size.toUInt() + println("zStream.next_out = ${zStream.next_out}, zStream.avail_out = ${zStream.avail_out}") + + val inflateResult = inflate(zStream.ptr, Z_NO_FLUSH) + println("inflate result: $inflateResult") + + when (inflateResult) { + Z_STREAM_END -> { + println("Z_STREAM_END") + val chunkSize = decompressedBuffer.size.toUInt() - zStream.avail_out + println("Z_STREAM_END chunkSize: ${decompressedBuffer.size.toUInt() - zStream.avail_out}") + output.addAll(decompressedBuffer.copyOf(chunkSize.toInt())) + return@usePinned true // Decompression complete + } + Z_OK -> { + println("Z_OK") + val chunkSize = decompressedBuffer.size.toUInt() - zStream.avail_out + println("Z_OK chunkSize: ${decompressedBuffer.size.toUInt() - zStream.avail_out}") + output.addAll(decompressedBuffer.copyOf(chunkSize.toInt())) + } + else -> throw IllegalStateException("Decompression failed with error code $inflateResult") + } + } + } + + println("output.size: ${output.size}") + return output.toUByteArray().toByteArray() + + } finally { + inflateEnd(zStream.ptr) + } + } } From 9ff5e325aeb1111e351688528510598bb790fa66 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 7 Jan 2025 13:23:04 -0500 Subject: [PATCH 12/58] Remove printlns and clean up --- .../kotlin/runtime/io/GzipTestUtilsNative.kt | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt b/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt index 7849bd63af..dcb3a7fe44 100644 --- a/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt +++ b/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt @@ -29,7 +29,6 @@ internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { opaque = null } - println("inflateInit2_") // Initialize the inflate context for gzip decoding val result = inflateInit2_( strm = zStream.ptr, @@ -42,42 +41,30 @@ internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { try { zStream.next_in = bytes.refTo(0).getPointer(memScope).reinterpret() zStream.avail_in = bytes.size.toUInt() - println("zStream.next_in = ${zStream.next_in}, zStream.avail_in = ${zStream.avail_in}") val output = mutableListOf() while (zStream.avail_in > 0u) { - println("loop zStream.avail_in = ${zStream.avail_in}") decompressedBuffer.usePinned { pinnedBuffer -> zStream.next_out = pinnedBuffer.addressOf(0) zStream.avail_out = decompressedBuffer.size.toUInt() - println("zStream.next_out = ${zStream.next_out}, zStream.avail_out = ${zStream.avail_out}") val inflateResult = inflate(zStream.ptr, Z_NO_FLUSH) - println("inflate result: $inflateResult") when (inflateResult) { - Z_STREAM_END -> { - println("Z_STREAM_END") + Z_OK, Z_STREAM_END -> { val chunkSize = decompressedBuffer.size.toUInt() - zStream.avail_out - println("Z_STREAM_END chunkSize: ${decompressedBuffer.size.toUInt() - zStream.avail_out}") - output.addAll(decompressedBuffer.copyOf(chunkSize.toInt())) - return@usePinned true // Decompression complete - } - Z_OK -> { - println("Z_OK") - val chunkSize = decompressedBuffer.size.toUInt() - zStream.avail_out - println("Z_OK chunkSize: ${decompressedBuffer.size.toUInt() - zStream.avail_out}") output.addAll(decompressedBuffer.copyOf(chunkSize.toInt())) } else -> throw IllegalStateException("Decompression failed with error code $inflateResult") } + + if (inflateResult == Z_STREAM_END) { + return@usePinned true + } } } - - println("output.size: ${output.size}") return output.toUByteArray().toByteArray() - } finally { inflateEnd(zStream.ptr) } From 28d46fc65e2a1a508dbb25d98c13eda38f83819a Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 7 Jan 2025 13:23:19 -0500 Subject: [PATCH 13/58] ktlint --- .../aws/smithy/kotlin/runtime/compression/GzipTest.kt | 2 +- .../smithy/kotlin/runtime/compression/GzipNative.kt | 2 +- .../kotlin/runtime/io/GzipByteReadChannelNative.kt | 4 +++- .../smithy/kotlin/runtime/io/GzipSdkSourceNative.kt | 4 +++- .../smithy/kotlin/runtime/io/GzipTestUtilsNative.kt | 11 +++++++---- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt index 14773ddc46..de1199a6bc 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt @@ -39,4 +39,4 @@ class GzipTest { assertContentEquals(payload, decompressedBytes) } -} \ No newline at end of file +} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index d1bbfac4a1..8ae22cd53e 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -75,7 +75,7 @@ internal fun gzipCompressBytes(bytes: ByteArray): ByteArray { memLevel = 9, strategy = Z_DEFAULT_STRATEGY, version = ZLIB_VERSION, - stream_size = sizeOf().toInt() + stream_size = sizeOf().toInt(), ) if (initResult != Z_OK) { throw IllegalStateException("Failed to initialize zlib with error code $initResult") diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt index b55b265030..2911844ed6 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt @@ -32,7 +32,9 @@ public actual class GzipByteReadChannel actual constructor(public val channel: S val temp = SdkBuffer() val rc = channel.read(temp, limit) - if (rc <= 0) { return rc } + if (rc <= 0) { + return rc + } val compressed = gzipCompressBytes(temp.readByteArray()) sink.write(compressed) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt index e67ff371c2..ebdcd199c8 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt @@ -20,7 +20,9 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc val temp = SdkBuffer() val rc = source.read(temp, limit) - if (rc <= 0L) { return rc } + if (rc <= 0L) { + return rc + } val compressed = gzipCompressBytes(temp.readByteArray()) sink.write(compressed) diff --git a/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt b/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt index dcb3a7fe44..f75d5e0055 100644 --- a/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt +++ b/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt @@ -18,7 +18,9 @@ import platform.zlib.* * Decompresses a byte array compressed using the gzip format */ internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { - if (bytes.isEmpty()) { return bytes } + if (bytes.isEmpty()) { + return bytes + } val decompressedBuffer = UByteArray(bytes.size * 2) // Initial guess for decompressed size (may expand) @@ -34,9 +36,11 @@ internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { strm = zStream.ptr, windowBits = 31, version = ZLIB_VERSION, - stream_size = sizeOf().toInt() + stream_size = sizeOf().toInt(), ) - if (result != Z_OK) { throw IllegalStateException("inflateInit2_ failed with error code $result") } + if (result != Z_OK) { + throw IllegalStateException("inflateInit2_ failed with error code $result") + } try { zStream.next_in = bytes.refTo(0).getPointer(memScope).reinterpret() @@ -44,7 +48,6 @@ internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { val output = mutableListOf() while (zStream.avail_in > 0u) { - decompressedBuffer.usePinned { pinnedBuffer -> zStream.next_out = pinnedBuffer.addressOf(0) zStream.avail_out = decompressedBuffer.size.toUInt() From 78f17f3dcb809e297669c9733e2f440da33985f2 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:23:33 -0800 Subject: [PATCH 14/58] chore: add *-main to branch workflows (#1209) --- .github/workflows/api-compat-verification.yml | 4 +++- .github/workflows/artifact-size-metrics.yml | 4 +++- .github/workflows/changelog-verification.yml | 4 +++- .github/workflows/continuous-integration.yml | 4 +++- .github/workflows/dependabot.yml | 4 +++- .github/workflows/kat-transform.yml | 4 +++- .github/workflows/lint.yml | 5 ++++- 7 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/api-compat-verification.yml b/.github/workflows/api-compat-verification.yml index 26c7ef3901..2d4a22452a 100644 --- a/.github/workflows/api-compat-verification.yml +++ b/.github/workflows/api-compat-verification.yml @@ -3,7 +3,9 @@ name: API compatibility verification on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' jobs: api-compat-verification: diff --git a/.github/workflows/artifact-size-metrics.yml b/.github/workflows/artifact-size-metrics.yml index bd1c521e16..0591b2b6aa 100644 --- a/.github/workflows/artifact-size-metrics.yml +++ b/.github/workflows/artifact-size-metrics.yml @@ -2,7 +2,9 @@ name: Artifact Size Metrics on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' release: types: [published] diff --git a/.github/workflows/changelog-verification.yml b/.github/workflows/changelog-verification.yml index 547b155abf..ed6efef911 100644 --- a/.github/workflows/changelog-verification.yml +++ b/.github/workflows/changelog-verification.yml @@ -3,7 +3,9 @@ name: Changelog verification on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' jobs: changelog-verification: diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9b0dc1c880..599293cc06 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -2,7 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: + - main + - '*-main' pull_request: workflow_dispatch: diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 69fb0c9885..6588de3857 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -2,7 +2,9 @@ name: Dependabot Dependency Submission on: push: - branches: [ main ] + branches: + - main + - '*-main' permissions: contents: write diff --git a/.github/workflows/kat-transform.yml b/.github/workflows/kat-transform.yml index 859a904bc3..938e936ef9 100644 --- a/.github/workflows/kat-transform.yml +++ b/.github/workflows/kat-transform.yml @@ -3,7 +3,9 @@ name: Kat Transform on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' # Allow one instance of this workflow per pull request, and cancel older runs when new changes are pushed concurrency: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6693ed2aae..e95b98aded 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,8 +5,11 @@ on: branches: - '**' - '!main' + - '!*-main' pull_request: - branches: [ main ] + branches: + - main + - '*-main' workflow_dispatch: env: From d06ee0a604dd0750421c6d7dfe7c47dfbcd062bf Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:23:43 -0800 Subject: [PATCH 15/58] chore: add .kotlin/ to .gitignore (#1208) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 225fe6070b..99ff20f122 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ out/ # Compiled class file *.class *.klib +.kotlin/ # Log file *.log From d2e7c918e7fc9ee9892fd6cd7d852db4d5b2f2db Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:16:58 -0500 Subject: [PATCH 16/58] chore: smithy version bump (#1213) --- .../model/error-correction-tests.smithy | 13 ++++++++++--- .../core/AwsHttpBindingProtocolGenerator.kt | 10 +++++++++- gradle/libs.versions.toml | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/codegen/protocol-tests/model/error-correction-tests.smithy b/codegen/protocol-tests/model/error-correction-tests.smithy index 74e614bdc5..201ffc877f 100644 --- a/codegen/protocol-tests/model/error-correction-tests.smithy +++ b/codegen/protocol-tests/model/error-correction-tests.smithy @@ -37,7 +37,12 @@ operation SayHello { output: TestOutputDocument, errors: [Error] } @http(method: "POST", uri: "/") operation SayHelloXml { output: TestOutput, errors: [Error] } -structure TestOutputDocument with [TestStruct] { innerField: Nested, @required document: Document } +structure TestOutputDocument with [TestStruct] { + innerField: Nested, + // FIXME: This trait fails smithy validator + // @required + document: Document +} structure TestOutput with [TestStruct] { innerField: Nested } @mixin @@ -60,7 +65,8 @@ structure TestStruct { @required nestedListValue: NestedList - @required + // FIXME: This trait fails smithy validator + // @required nested: Nested @required @@ -91,7 +97,8 @@ union MyUnion { } structure Nested { - @required + // FIXME: This trait fails smithy validator + // @required a: String } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt index 864c2a6c3c..6e5834f7d1 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt @@ -39,7 +39,15 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator() // The following can be used to generate only a specific test by name. // val targetedTest = TestMemberDelta(setOf("RestJsonComplexErrorWithNoMessage"), TestContainmentMode.RUN_TESTS) - val ignoredTests = TestMemberDelta(setOf()) + val ignoredTests = TestMemberDelta( + setOf( + "AwsJson10ClientErrorCorrectsWithDefaultValuesWhenServerFailsToSerializeRequiredValues", + "RestJsonNullAndEmptyHeaders", + "NullAndEmptyHeaders", + "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", + "RpcV2CborClientPopulatesDefaultValuesInInput", + ), + ) val requestTestBuilder = HttpProtocolUnitTestRequestGenerator.Builder() val responseTestBuilder = HttpProtocolUnitTestResponseGenerator.Builder() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0683bfc2e1..14232f4241 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ crt-kotlin-version = "0.8.10" micrometer-version = "1.13.6" # codegen -smithy-version = "1.51.0" +smithy-version = "1.53.0" smithy-gradle-version = "0.9.0" # testing From 5a2df8f171250e150c685c9389f80e2a6b0778eb Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 14:36:46 +0000 Subject: [PATCH 17/58] chore: release 1.3.33 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a52fe756c1..2c327d4f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.3.33] - 01/10/2025 + ## [1.3.32] - 01/06/2025 ### Fixes diff --git a/gradle.properties b/gradle.properties index b49ffff27f..b6869a974b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.33-SNAPSHOT +sdkVersion=1.3.33 # codegen -codegenVersion=0.33.33-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.33 \ No newline at end of file From 003633bad25b5b3e75112da2d1de5f2eeffd1471 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 14:36:47 +0000 Subject: [PATCH 18/58] chore: bump snapshot version to 1.3.34-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index b6869a974b..6c237cf8df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.33 +sdkVersion=1.3.34-SNAPSHOT # codegen -codegenVersion=0.33.33 \ No newline at end of file +codegenVersion=0.33.34-SNAPSHOT \ No newline at end of file From 5f5ec8f16b4db571bb9c83f520e26f9927d8ecf2 Mon Sep 17 00:00:00 2001 From: Matas Date: Fri, 10 Jan 2025 11:13:29 -0500 Subject: [PATCH 19/58] feat: add `AuthTokenGenerator` (#1212) --- .../api/aws-signing-common.api | 10 +++ .../auth/awssigning/AuthTokenGenerator.kt | 44 ++++++++++++ .../auth/awssigning/AuthTokenGeneratorTest.kt | 71 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt create mode 100644 runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt diff --git a/runtime/auth/aws-signing-common/api/aws-signing-common.api b/runtime/auth/aws-signing-common/api/aws-signing-common.api index 0c96486679..d128b0bd6f 100644 --- a/runtime/auth/aws-signing-common/api/aws-signing-common.api +++ b/runtime/auth/aws-signing-common/api/aws-signing-common.api @@ -1,3 +1,13 @@ +public final class aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator { + public fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;)V + public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun generateAuthToken-exY8QGI (Laws/smithy/kotlin/runtime/net/url/Url;Ljava/lang/String;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getClock ()Laws/smithy/kotlin/runtime/time/Clock; + public final fun getCredentialsProvider ()Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider; + public final fun getService ()Ljava/lang/String; + public final fun getSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; +} + public final class aws/smithy/kotlin/runtime/auth/awssigning/AwsChunkedByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel { public fun (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;)V public synthetic fun (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt new file mode 100644 index 0000000000..e92ac1f72c --- /dev/null +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -0,0 +1,44 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Clock +import kotlin.time.Duration + +/** + * Generates an authentication token, which is a SigV4-signed URL with the HTTP scheme removed. + * @param service The name of the service the token is being generated for + * @param credentialsProvider The [CredentialsProvider] which will provide credentials to use when generating the auth token + * @param signer The [AwsSigner] implementation to use when creating the authentication token + * @param clock The [Clock] implementation to use + */ +public class AuthTokenGenerator( + public val service: String, + public val credentialsProvider: CredentialsProvider, + public val signer: AwsSigner, + public val clock: Clock = Clock.System, +) { + private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://") + + public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { + val req = HttpRequest(HttpMethod.GET, endpoint) + + val config = AwsSigningConfig { + credentials = credentialsProvider.resolve() + this.region = region + service = this@AuthTokenGenerator.service + signingDate = clock.now() + expiresAfter = expiration + signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS + } + + return signer.sign(req, config).output.url.trimScheme() + } +} diff --git a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt new file mode 100644 index 0000000000..87fec1a74e --- /dev/null +++ b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt @@ -0,0 +1,71 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.collections.Attributes +import aws.smithy.kotlin.runtime.http.Headers +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.net.Host +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.time.ManualClock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit + +class AuthTokenGeneratorTest { + @Test + fun testGenerateAuthToken() = runTest { + val credentials = Credentials("akid", "secret") + + val credentialsProvider = object : CredentialsProvider { + var credentialsResolved = false + override suspend fun resolve(attributes: Attributes): Credentials { + credentialsResolved = true + return credentials + } + } + + val clock = ManualClock(Instant.fromEpochSeconds(0)) + + val generator = AuthTokenGenerator("foo", credentialsProvider, TEST_SIGNER, clock = clock) + + val endpoint = Url { host = Host.parse("foo.bar.us-east-1.baz") } + val token = generator.generateAuthToken(endpoint, "us-east-1", 333.seconds) + + assertContains(token, "foo.bar.us-east-1.baz") + assertContains(token, "X-Amz-Credential=signature") // test custom signer was invoked + assertContains(token, "X-Amz-Expires=333") // expiration + assertContains(token, "X-Amz-SigningDate=0") // clock + + assertTrue(credentialsProvider.credentialsResolved) + } +} + +private val TEST_SIGNER = object : AwsSigner { + override suspend fun sign( + request: HttpRequest, + config: AwsSigningConfig, + ): AwsSigningResult { + val builder = request.toBuilder() + builder.url.parameters.decodedParameters.apply { + put("X-Amz-Credential", "signature") + put("X-Amz-Expires", (config.expiresAfter?.toLong(DurationUnit.SECONDS) ?: 900).toString()) + put("X-Amz-SigningDate", config.signingDate.epochSeconds.toString()) + } + + return AwsSigningResult(builder.build(), "signature".encodeToByteArray()) + } + + override suspend fun signChunk(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult = throw IllegalStateException("signChunk unexpectedly invoked") + + override suspend fun signChunkTrailer(trailingHeaders: Headers, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult = throw IllegalStateException("signChunkTrailer unexpectedly invoked") +} From 3fe1b5e7763bd4ed386f30d3f4bfcbb0ad4aa352 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 16:45:39 +0000 Subject: [PATCH 20/58] chore: release 1.3.34 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c327d4f40..c1758b4332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.3.34] - 01/10/2025 + ## [1.3.33] - 01/10/2025 ## [1.3.32] - 01/06/2025 diff --git a/gradle.properties b/gradle.properties index 6c237cf8df..193406ed69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.34-SNAPSHOT +sdkVersion=1.3.34 # codegen -codegenVersion=0.33.34-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.34 \ No newline at end of file From 0bba3084ebb3160abe2a21fc86f4223c7bf37c23 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 16:45:40 +0000 Subject: [PATCH 21/58] chore: bump snapshot version to 1.3.35-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 193406ed69..d76819491b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.34 +sdkVersion=1.3.35-SNAPSHOT # codegen -codegenVersion=0.33.34 \ No newline at end of file +codegenVersion=0.33.35-SNAPSHOT \ No newline at end of file From 52e4439088b388c5f4d8c9b1e6fec6588d37a1a4 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:33:57 -0500 Subject: [PATCH 22/58] misc: merge v1.4 into main (#1218) --- .brazil.json | 2 +- .../0857b6f0-0444-479f-be6b-06ef71d482a0.json | 9 ++++ .../1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json | 6 +++ .../3456d00f-1b29-4d88-ab02-045db1b1ebce.json | 8 +++ .../kotlin/codegen/core/KotlinDependency.kt | 2 +- .../rendering/waiters/WaiterGenerator.kt | 15 +++--- .../waiters/ServiceWaitersGeneratorTest.kt | 6 +-- .../rendering/waiters/WaiterGeneratorTest.kt | 4 +- gradle/libs.versions.toml | 17 ++++--- .../tests/SigningSuiteTestBaseJVM.kt | 3 +- runtime/runtime-core/api/runtime-core.api | 15 +++++- .../aws/smithy/kotlin/runtime/Exceptions.kt | 49 +++++++++++++++++- .../kotlin/runtime/collections/Attributes.kt | 11 ++++ .../kotlin/runtime/retries/Exceptions.kt | 23 ++++++++- .../runtime/retries/StandardRetryStrategy.kt | 30 +++++++++-- .../retries/delay/StandardRetryTokenBucket.kt | 2 +- .../smithy/kotlin/runtime/ExceptionsTest.kt | 34 +++++++++++++ .../impl/StandardRetryIntegrationTest.kt | 50 +++++++++++++++---- .../smithy/kotlin/codegen/util/TestUtils.kt | 7 +-- 19 files changed, 250 insertions(+), 43 deletions(-) create mode 100644 .changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json create mode 100644 .changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json create mode 100644 .changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json diff --git a/.brazil.json b/.brazil.json index cb2e43713a..5a1d830abd 100644 --- a/.brazil.json +++ b/.brazil.json @@ -1,6 +1,6 @@ { "dependencies": { - "org.jetbrains.kotlin:kotlin-stdlib:2.0.*": "KotlinStdlib-2.x", + "org.jetbrains.kotlin:kotlin-stdlib:2.*": "KotlinStdlib-2.x", "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.*": "KotlinxCoroutinesCoreJvm-1.x", "com.squareup.okhttp3:okhttp-coroutines:5.*": "OkHttp3Coroutines-5.x", diff --git a/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json b/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json new file mode 100644 index 0000000000..d89da89103 --- /dev/null +++ b/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json @@ -0,0 +1,9 @@ +{ + "id": "0857b6f0-0444-479f-be6b-06ef71d482a0", + "type": "feature", + "description": "⚠️ **IMPORTANT**: Add `retryStrategy` configuration option for waiters", + "issues": [ + "https://github.com/awslabs/aws-sdk-kotlin/issues/1431" + ], + "requiresMinorVersionBump": true +} \ No newline at end of file diff --git a/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json b/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json new file mode 100644 index 0000000000..10ef502204 --- /dev/null +++ b/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json @@ -0,0 +1,6 @@ +{ + "id": "1a68d0b7-00e7-45c0-88f6-95e5c39c9c61", + "type": "misc", + "description": "⚠️ **IMPORTANT**: Upgrade to Kotlin 2.1.0", + "requiresMinorVersionBump": true +} \ No newline at end of file diff --git a/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json b/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json new file mode 100644 index 0000000000..767e48a02e --- /dev/null +++ b/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json @@ -0,0 +1,8 @@ +{ + "id": "3456d00f-1b29-4d88-ab02-045db1b1ebce", + "type": "bugfix", + "description": "Include more information when retry strategy halts early due to token bucket capacity errors", + "issues": [ + "awslabs/aws-sdk-kotlin#1321" + ] +} \ No newline at end of file diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt index 17faf0ad48..b575efae60 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt @@ -37,7 +37,7 @@ private fun getDefaultRuntimeVersion(): String { // publishing info const val RUNTIME_GROUP: String = "aws.smithy.kotlin" val RUNTIME_VERSION: String = System.getProperty("smithy.kotlin.codegen.clientRuntimeVersion", getDefaultRuntimeVersion()) -val KOTLIN_COMPILER_VERSION: String = System.getProperty("smithy.kotlin.codegen.kotlinCompilerVersion", "2.0.10") +val KOTLIN_COMPILER_VERSION: String = System.getProperty("smithy.kotlin.codegen.kotlinCompilerVersion", "2.1.0") enum class SourceSet { CommonMain, diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt index 552a352a1a..f150cbc3d2 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt @@ -17,7 +17,7 @@ import java.text.DecimalFormatSymbols * Renders the top-level retry strategy for a waiter. */ private fun KotlinWriter.renderRetryStrategy(wi: WaiterInfo, asValName: String) { - withBlock("val #L = #T {", "}", asValName, RuntimeTypes.Core.Retries.StandardRetryStrategy) { + withBlock("val #L = retryStrategy ?: #T {", "}", asValName, RuntimeTypes.Core.Retries.StandardRetryStrategy) { write("maxAttempts = 20") write("tokenBucket = #T", RuntimeTypes.Core.Retries.Delay.InfiniteTokenBucket) withBlock("delayProvider {", "}") { @@ -35,18 +35,21 @@ private fun KotlinWriter.renderRetryStrategy(wi: WaiterInfo, asValName: String) internal fun KotlinWriter.renderWaiter(wi: WaiterInfo) { write("") wi.waiter.documentation.ifPresent(::dokka) - val inputParameter = if (wi.input.hasAllOptionalMembers) { - format("request: #1T = #1T { }", wi.inputSymbol) + + val requestType = if (wi.input.hasAllOptionalMembers) { + format("#1T = #1T { }", wi.inputSymbol) } else { - format("request: #T", wi.inputSymbol) + format("#T", wi.inputSymbol) } + withBlock( - "#L suspend fun #T.#L(#L): #T<#T> {", + "#L suspend fun #T.#L(request: #L, retryStrategy: #T? = null): #T<#T> {", "}", wi.ctx.settings.api.visibility, wi.serviceSymbol, wi.methodName, - inputParameter, + requestType, + RuntimeTypes.Core.Retries.RetryStrategy, RuntimeTypes.Core.Retries.Outcome, wi.outputSymbol, ) { diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt index c83c29661d..49ee90f484 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt @@ -43,7 +43,7 @@ class ServiceWaitersGeneratorTest { /** * Wait until a foo exists with optional input */ - public suspend fun TestClient.waitUntilFooOptionalExists(request: DescribeFooOptionalRequest = DescribeFooOptionalRequest { }): Outcome { + public suspend fun TestClient.waitUntilFooOptionalExists(request: DescribeFooOptionalRequest = DescribeFooOptionalRequest { }, retryStrategy: RetryStrategy? = null): Outcome { """.trimIndent() val methodFooter = """ val policy = AcceptorRetryPolicy(request, acceptors) @@ -59,7 +59,7 @@ class ServiceWaitersGeneratorTest { /** * Wait until a foo exists with required input */ - public suspend fun TestClient.waitUntilFooRequiredExists(request: DescribeFooRequiredRequest): Outcome { + public suspend fun TestClient.waitUntilFooRequiredExists(request: DescribeFooRequiredRequest, retryStrategy: RetryStrategy? = null): Outcome { """.trimIndent() listOf( generateService("simple-service-with-operation-waiter.smithy"), @@ -105,7 +105,7 @@ class ServiceWaitersGeneratorTest { @Test fun testRetryStrategy() { val expected = """ - val strategy = StandardRetryStrategy { + val strategy = retryStrategy ?: StandardRetryStrategy { maxAttempts = 20 tokenBucket = InfiniteTokenBucket delayProvider { diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt index c8f6ae3cfc..705b3b8b16 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt @@ -28,7 +28,7 @@ class WaiterGeneratorTest { @Test fun testDefaultDelays() { val expected = """ - val strategy = StandardRetryStrategy { + val strategy = retryStrategy ?: StandardRetryStrategy { maxAttempts = 20 tokenBucket = InfiniteTokenBucket delayProvider { @@ -45,7 +45,7 @@ class WaiterGeneratorTest { @Test fun testCustomDelays() { val expected = """ - val strategy = StandardRetryStrategy { + val strategy = retryStrategy ?: StandardRetryStrategy { maxAttempts = 20 tokenBucket = InfiniteTokenBucket delayProvider { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 14232f4241..6ef5b35d57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -kotlin-version = "2.0.21" +kotlin-version = "2.1.0" dokka-version = "1.9.10" -aws-kotlin-repo-tools-version = "0.4.16" +aws-kotlin-repo-tools-version = "0.4.17" # libs coroutines-version = "1.9.0" @@ -10,11 +10,12 @@ atomicfu-version = "0.25.0" okhttp-version = "5.0.0-alpha.14" okhttp4-version = "4.12.0" okio-version = "3.9.1" -otel-version = "1.43.0" +otel-version = "1.45.0" slf4j-version = "2.0.16" slf4j-v1x-version = "1.7.36" -crt-kotlin-version = "0.8.10" -micrometer-version = "1.13.6" +crt-kotlin-version = "0.9.0" +micrometer-version = "1.14.2" +binary-compatibility-validator-version = "0.16.3" # codegen smithy-version = "1.53.0" @@ -23,7 +24,7 @@ smithy-gradle-version = "0.9.0" # testing junit-version = "5.10.5" kotest-version = "5.9.1" -kotlin-compile-testing-version = "1.6.0" +kotlin-compile-testing-version = "0.7.0" kotlinx-benchmark-version = "0.4.12" kotlinx-serialization-version = "1.7.3" docker-java-version = "3.4.0" @@ -80,7 +81,7 @@ smithy-smoke-test-traits = { module = "software.amazon.smithy:smithy-smoke-test- junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-version" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-version" } -kotlin-compile-testing = {module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kotlin-compile-testing-version" } +kotlin-compile-testing = {module = "dev.zacsweers.kctfork:core", version.ref = "kotlin-compile-testing-version" } kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest-version" } kotest-assertions-core-jvm = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest-version" } kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark-version" } @@ -104,7 +105,7 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka-version"} kotlin-jvm = {id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" } kotlin-multiplatform = {id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin-version" } kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlinx-benchmark-version" } -kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.13.2" } +kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator-version" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-version"} aws-kotlin-repo-tools-kmp = { id = "aws.sdk.kotlin.gradle.kmp", version.ref = "aws-kotlin-repo-tools-version" } aws-kotlin-repo-tools-smithybuild = { id = "aws.sdk.kotlin.gradle.smithybuild", version.ref = "aws-kotlin-repo-tools-version" } diff --git a/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt b/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt index 904aef564b..b3c4202d26 100644 --- a/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt +++ b/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt @@ -28,6 +28,7 @@ import io.ktor.util.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* import kotlinx.coroutines.runBlocking +import kotlinx.io.readByteArray import kotlinx.serialization.json.* import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Test @@ -383,7 +384,7 @@ public actual abstract class SigningSuiteTestBase : HasSigner { } if (hasBody) { - val bytes = runBlocking { chan.readRemaining().readBytes() } + val bytes = runBlocking { chan.readRemaining().readByteArray() } builder.body = HttpBody.fromBytes(bytes) } diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 942f99e348..8c42034110 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -1,3 +1,13 @@ +public final class aws/smithy/kotlin/runtime/ClientErrorContext { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getFormatted ()Ljava/lang/String; + public final fun getKey ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public class aws/smithy/kotlin/runtime/ClientException : aws/smithy/kotlin/runtime/SdkBaseException { public fun ()V public fun (Ljava/lang/String;)V @@ -9,11 +19,13 @@ public class aws/smithy/kotlin/runtime/ErrorMetadata { public static final field Companion Laws/smithy/kotlin/runtime/ErrorMetadata$Companion; public fun ()V public final fun getAttributes ()Laws/smithy/kotlin/runtime/collections/MutableAttributes; + public final fun getClientContext ()Ljava/util/List; public final fun isRetryable ()Z public final fun isThrottling ()Z } public final class aws/smithy/kotlin/runtime/ErrorMetadata$Companion { + public final fun getClientContext ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getRetryable ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getThrottlingError ()Laws/smithy/kotlin/runtime/collections/AttributeKey; } @@ -62,7 +74,6 @@ public class aws/smithy/kotlin/runtime/ServiceException : aws/smithy/kotlin/runt public fun (Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/lang/Throwable;)V public fun (Ljava/lang/Throwable;)V - protected fun getDisplayMetadata ()Ljava/util/List; public fun getMessage ()Ljava/lang/String; public synthetic fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ErrorMetadata; public fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ServiceErrorMetadata; @@ -134,6 +145,7 @@ public final class aws/smithy/kotlin/runtime/collections/AttributesBuilder { } public final class aws/smithy/kotlin/runtime/collections/AttributesKt { + public static final fun appendValue (Laws/smithy/kotlin/runtime/collections/MutableAttributes;Laws/smithy/kotlin/runtime/collections/AttributeKey;Ljava/lang/Object;)V public static final fun attributesOf (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/collections/Attributes; public static final fun emptyAttributes ()Laws/smithy/kotlin/runtime/collections/Attributes; public static final fun get (Laws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/collections/AttributeKey;)Ljava/lang/Object; @@ -1685,6 +1697,7 @@ public abstract class aws/smithy/kotlin/runtime/retries/RetryException : aws/smi public final fun getAttempts ()I public final fun getLastException ()Ljava/lang/Throwable; public final fun getLastResponse ()Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/smithy/kotlin/runtime/retries/RetryFailureException : aws/smithy/kotlin/runtime/retries/RetryException { diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt index 87697ec7d7..661d855a5b 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt @@ -8,6 +8,41 @@ import aws.smithy.kotlin.runtime.collections.AttributeKey import aws.smithy.kotlin.runtime.collections.MutableAttributes import aws.smithy.kotlin.runtime.collections.mutableAttributes +/** + * Describes additional context about an error which may be useful in client-side debugging. This information will be + * included in exception messages. This contrasts with [ErrorMetadata] which is not _necessarily_ included in messages + * and not _necessarily_ client-related. + * @param key A header or key for the information + * @param value A value for the information + */ +public class ClientErrorContext(public val key: String, public val value: String) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ClientErrorContext + + if (key != other.key) return false + if (value != other.value) return false + + return true + } + + /** + * Gets a formatted representation of this error context suitable for inclusion in a message. This format is + * generally `"$key: $value"`. + */ + public val formatted: String = "$key: $value" + + override fun hashCode(): Int { + var result = key.hashCode() + result = 31 * result + value.hashCode() + return result + } + + override fun toString(): String = "ClientErrorContext(key='$key', value='$value')" +} + /** * Additional metadata about an error */ @@ -16,6 +51,12 @@ public open class ErrorMetadata { public val attributes: MutableAttributes = mutableAttributes() public companion object { + /** + * Set if there are additional context elements about the error + */ + public val ClientContext: AttributeKey> = + AttributeKey("aws.smithy.kotlin#ClientContext") + /** * Set if an error is retryable */ @@ -32,6 +73,9 @@ public open class ErrorMetadata { public val isThrottling: Boolean get() = attributes.getOrNull(ThrottlingError) ?: false + + public val clientContext: List + get() = attributes.getOrNull(ClientContext).orEmpty() } /** @@ -156,7 +200,7 @@ public open class ServiceException : SdkBaseException { public constructor(cause: Throwable?) : super(cause) - protected open val displayMetadata: List + private val displayMetadata: List get() = buildList { val serviceProvidedMessage = super.message ?: sdkErrorMetadata.errorMessage if (serviceProvidedMessage == null) { @@ -166,7 +210,10 @@ public open class ServiceException : SdkBaseException { } else { add(serviceProvidedMessage) } + sdkErrorMetadata.requestId?.let { add("Request ID: $it") } + + sdkErrorMetadata.clientContext.mapTo(this@buildList) { it.formatted } } override val message: String diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt index 1fffcc82a2..77c2507394 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt @@ -115,6 +115,17 @@ public fun MutableAttributes.merge(other: Attributes) { } } +/** + * Appends a value to a list-typed attribute. If the attribute does not exist, it will be created. + * @param key The key for the attribute + * @param element The element to append to the existing (or new) list value of the attribute + */ +public fun MutableAttributes.appendValue(key: AttributeKey>, element: E) { + val existingList = getOrNull(key).orEmpty() + val newList = existingList + element + set(key, newList) +} + private class AttributesImpl constructor(seed: Attributes) : MutableAttributes { private val map: MutableMap, Any> = mutableMapOf() constructor() : this(emptyAttributes()) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt index dbc2de0ffa..a81c830da2 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt @@ -22,7 +22,28 @@ public sealed class RetryException( public val attempts: Int, public val lastResponse: Any?, public val lastException: Throwable?, -) : ClientException(message, cause) +) : ClientException(message, cause) { + override fun toString(): String = buildString { + append(this@RetryException::class.simpleName) + append("(") + + append("message=") + append(message) + + append(",attempts=") + append(attempts) + + if (lastException != null) { + append(",lastException=") + append(lastException) + } else if (lastResponse != null) { + append(",lastResponse=") + append(lastResponse) + } + + append(")") + } +} /** * Indicates that retrying has failed because too many attempts have completed unsuccessfully. diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt index 95fd16faee..fb223aec68 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt @@ -5,6 +5,10 @@ package aws.smithy.kotlin.runtime.retries +import aws.smithy.kotlin.runtime.ClientErrorContext +import aws.smithy.kotlin.runtime.ErrorMetadata +import aws.smithy.kotlin.runtime.ServiceException +import aws.smithy.kotlin.runtime.collections.appendValue import aws.smithy.kotlin.runtime.retries.delay.* import aws.smithy.kotlin.runtime.retries.policy.RetryDirective import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy @@ -129,17 +133,35 @@ public open class StandardRetryStrategy(override val config: Config = Config.def } } - private fun throwCapacityExceeded(cause: Throwable, attempt: Int, result: Result?): Nothing = - when (val ex = result?.exceptionOrNull()) { + private fun throwCapacityExceeded( + cause: RetryCapacityExceededException, + attempt: Int, + result: Result?, + ): Nothing { + val capacityMessage = buildString { + append("Insufficient client capacity to attempt retry, halting on attempt ") + append(attempt) + append(" of ") + append(config.maxAttempts) + } + + throw when (val retryableException = result?.exceptionOrNull()) { null -> throw TooManyAttemptsException( - cause.message!!, + capacityMessage, cause, attempt, result?.getOrNull(), result?.exceptionOrNull(), ) - else -> throw ex + + is ServiceException -> retryableException.apply { + val addCtx = ClientErrorContext("Early retry termination", capacityMessage) + sdkErrorMetadata.attributes.appendValue(ErrorMetadata.ClientContext, addCtx) + } + + else -> retryableException } + } /** * Handles the termination of the retry loop because of a non-retryable failure by throwing a diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt index 4bd2c23766..b2c4c70588 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt @@ -60,7 +60,7 @@ public class StandardRetryTokenBucket internal constructor( capacity -= size } else { if (config.useCircuitBreakerMode) { - throw RetryCapacityExceededException("Insufficient capacity to attempt another retry") + throw RetryCapacityExceededException("Insufficient capacity to attempt retry") } val extraRequiredCapacity = size - capacity diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt index 551718c36e..0ba2d1639f 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt @@ -5,9 +5,14 @@ package aws.smithy.kotlin.runtime import aws.smithy.kotlin.runtime.collections.MutableAttributes +import aws.smithy.kotlin.runtime.collections.appendValue import kotlin.test.Test import kotlin.test.assertEquals +private const val CTX_KEY_1 = "Color" +private const val CTX_VALUE_1 = "blue" +private const val CTX_KEY_2 = "Shape" +private const val CTX_VALUE_2 = "square" private const val ERROR_CODE = "ErrorWithNoMessage" private const val METADATA_MESSAGE = "This is a message included in metadata but not the regular response" private const val PROTOCOL_RESPONSE_SUMMARY = "HTTP 418 I'm a teapot" @@ -104,6 +109,35 @@ class ExceptionsTest { e.message, ) } + + @Test + fun testNoMessageWithClientContext() { + val e = FooServiceException { + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_1, CTX_VALUE_1)) + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_2, CTX_VALUE_2)) + } + assertEquals( + buildList { + add("Error type: Unknown") + add("Protocol response: (empty response)") + add("$CTX_KEY_1: $CTX_VALUE_1") + add("$CTX_KEY_2: $CTX_VALUE_2") + }.joinToString(), + e.message, + ) + } + + @Test + fun testMessageWithClientContext() { + val e = FooServiceException(SERVICE_MESSAGE) { + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_1, CTX_VALUE_1)) + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_2, CTX_VALUE_2)) + } + assertEquals( + "$SERVICE_MESSAGE, $CTX_KEY_1: $CTX_VALUE_1, $CTX_KEY_2: $CTX_VALUE_2", + e.message, + ) + } } private class FooServiceException( diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt index f2c08e47eb..2bd3bc7e6a 100644 --- a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt +++ b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt @@ -5,8 +5,8 @@ package aws.smithy.kotlin.runtime.retries.impl +import aws.smithy.kotlin.runtime.ServiceException import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy -import aws.smithy.kotlin.runtime.retries.TooManyAttemptsException import aws.smithy.kotlin.runtime.retries.delay.StandardRetryTokenBucket import aws.smithy.kotlin.runtime.retries.getOrThrow import aws.smithy.kotlin.runtime.retries.policy.RetryDirective @@ -15,6 +15,7 @@ import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.assertThrows import kotlin.test.* import kotlin.time.Duration.Companion.milliseconds @@ -38,7 +39,7 @@ class StandardRetryIntegrationTest { val block = object { var index = 0 - suspend fun doIt() = tc.responses[index++].response.statusCode + suspend fun doIt() = tc.responses[index++].response.getOrThrow() }::doIt val startTimeMs = currentTime @@ -47,9 +48,29 @@ class StandardRetryIntegrationTest { val finalState = tc.responses.last().expected when (finalState.outcome) { - TestOutcome.Success -> assertEquals(200, result.getOrNull()?.getOrThrow(), "Unexpected outcome for $name") - TestOutcome.MaxAttemptsExceeded -> assertIs(result.exceptionOrNull()) - TestOutcome.RetryQuotaExceeded -> assertIs(result.exceptionOrNull()) + TestOutcome.Success -> + assertEquals(Ok, result.getOrThrow().getOrThrow(), "Unexpected outcome for $name") + + TestOutcome.MaxAttemptsExceeded -> { + val e = assertThrows("Expected exception for $name") { + result.getOrThrow() + } + + assertEquals(tc.responses.last().response.statusCode, e.code, "Unexpected error code for $name") + } + + TestOutcome.RetryQuotaExceeded -> { + val e = assertThrows("Expected exception for $name") { + result.getOrThrow() + } + + assertEquals(tc.responses.last().response.statusCode, e.code, "Unexpected error code for $name") + + assertTrue("Expected retry capacity message in exception for $name") { + "Insufficient client capacity to attempt retry" in e.message + } + } + else -> fail("Unexpected outcome for $name: ${finalState.outcome}") } @@ -72,10 +93,19 @@ class StandardRetryIntegrationTest { } } -object IntegrationTestPolicy : RetryPolicy { - override fun evaluate(result: Result): RetryDirective = when (val code = result.getOrNull()!!) { - 200 -> RetryDirective.TerminateAndSucceed - 500, 502 -> RetryDirective.RetryError(RetryErrorType.ServerSide) - else -> fail("Unexpected status code: $code") +object IntegrationTestPolicy : RetryPolicy { + override fun evaluate(result: Result): RetryDirective = when { + result.isSuccess -> RetryDirective.TerminateAndSucceed + result.isFailure -> RetryDirective.RetryError(RetryErrorType.ServerSide) + else -> fail("Unexpected result condition") } } + +data object Ok + +class HttpCodeException(val code: Int) : ServiceException() + +fun Response.getOrThrow() = when (statusCode) { + 200 -> Ok + else -> throw HttpCodeException(statusCode) +} diff --git a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt index 02bf209f3c..2e64c9d4fe 100644 --- a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt +++ b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt @@ -6,6 +6,7 @@ package software.amazon.smithy.kotlin.codegen.util +import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi @@ -36,8 +37,8 @@ private fun String.slashEscape(char: Char) = this.replace(char.toString(), """\$ * Captures the result of a model transformation test */ data class ModelChangeTestResult( - val originalModelCompilationResult: KotlinCompilation.Result, - val updatedModelCompilationResult: KotlinCompilation.Result, + val originalModelCompilationResult: JvmCompilationResult, + val updatedModelCompilationResult: JvmCompilationResult, val compileSuccess: Boolean, val compileOutput: String, ) @@ -88,7 +89,7 @@ fun compileSdkAndTest( testSource: String? = null, outputSink: OutputStream = System.out, emitSourcesToTmp: Boolean = false, -): KotlinCompilation.Result { +): JvmCompilationResult { val sdkFileManifest = generateSdk(model) if (emitSourcesToTmp) { From e0c25d654adde3124f50733e0bc66fde2ee8d058 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:35:50 -0500 Subject: [PATCH 23/58] feat: support default checksums (#1191) --- .../kotlin/codegen/core/RuntimeTypes.kt | 7 +- .../HttpChecksumRequiredIntegration.kt | 66 +++++ .../protocol/HttpProtocolClientGenerator.kt | 25 -- ...tlin.codegen.integration.KotlinIntegration | 1 + .../protocol/http-client/api/http-client.api | 20 +- ...eptor.kt => CachingChecksumInterceptor.kt} | 14 +- .../interceptors/ChecksumInterceptorUtils.kt | 44 +++ .../FlexibleChecksumsRequestInterceptor.kt | 277 ++++++++---------- .../FlexibleChecksumsResponseInterceptor.kt | 94 +++--- .../HttpChecksumRequiredInterceptor.kt | 98 +++++++ .../interceptors/Md5ChecksumInterceptor.kt | 54 ---- .../http/operation/HttpOperationContext.kt | 4 +- ...t.kt => CachingChecksumInterceptorTest.kt} | 12 +- ...FlexibleChecksumsRequestInterceptorTest.kt | 115 +++++--- ...lexibleChecksumsResponseInterceptorTest.kt | 84 +++++- ...=> HttpChecksumRequiredInterceptorTest.kt} | 46 ++- runtime/protocol/http/api/http.api | 18 ++ .../smithy/kotlin/runtime/http/HttpBody.kt | 52 ++++ runtime/runtime-core/api/runtime-core.api | 15 + .../businessmetrics/BusinessMetricsUtils.kt | 8 + .../kotlin/runtime/hashing/HashFunction.kt | 55 ++++ .../kotlin/runtime/io/SdkByteReadChannel.kt | 16 + runtime/smithy-client/api/smithy-client.api | 28 ++ .../client/config/HttpChecksumConfig.kt | 63 ++++ 24 files changed, 865 insertions(+), 351 deletions(-) create mode 100644 codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt rename runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/{AbstractChecksumInterceptor.kt => CachingChecksumInterceptor.kt} (67%) create mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt create mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt delete mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt rename runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/{AbstractChecksumInterceptorTest.kt => CachingChecksumInterceptorTest.kt} (88%) rename runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/{Md5ChecksumInterceptorTest.kt => HttpChecksumRequiredInterceptorTest.kt} (62%) create mode 100644 runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt index e42612b0b5..b6341e0443 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt @@ -81,7 +81,7 @@ object RuntimeTypes { object Interceptors : RuntimeTypePackage(KotlinDependency.HTTP, "interceptors") { val ContinueInterceptor = symbol("ContinueInterceptor") val HttpInterceptor = symbol("HttpInterceptor") - val Md5ChecksumInterceptor = symbol("Md5ChecksumInterceptor") + val HttpChecksumRequiredInterceptor = symbol("HttpChecksumRequiredInterceptor") val FlexibleChecksumsRequestInterceptor = symbol("FlexibleChecksumsRequestInterceptor") val FlexibleChecksumsResponseInterceptor = symbol("FlexibleChecksumsResponseInterceptor") val ResponseLengthValidationInterceptor = symbol("ResponseLengthValidationInterceptor") @@ -231,6 +231,9 @@ object RuntimeTypes { object Config : RuntimeTypePackage(KotlinDependency.SMITHY_CLIENT, "config") { val RequestCompressionConfig = symbol("RequestCompressionConfig") val CompressionClientConfig = symbol("CompressionClientConfig") + val HttpChecksumConfig = symbol("HttpChecksumConfig") + val RequestHttpChecksumConfig = symbol("RequestHttpChecksumConfig") + val ResponseHttpChecksumConfig = symbol("ResponseHttpChecksumConfig") } object Endpoints : RuntimeTypePackage(KotlinDependency.SMITHY_CLIENT, "endpoints") { @@ -395,6 +398,7 @@ object RuntimeTypes { val TelemetryContextElement = symbol("TelemetryContextElement", "context") val TraceSpan = symbol("TraceSpan", "trace") val withSpan = symbol("withSpan", "trace") + val warn = symbol("warn", "logging") } object TelemetryDefaults : RuntimeTypePackage(KotlinDependency.TELEMETRY_DEFAULTS) { val Global = symbol("Global") @@ -409,6 +413,7 @@ object RuntimeTypes { val CompletableDeferred = "kotlinx.coroutines.CompletableDeferred".toSymbol() val job = "kotlinx.coroutines.job".toSymbol() + val runBlocking = "kotlinx.coroutines.runBlocking".toSymbol() object Flow { // NOTE: smithy-kotlin core has an API dependency on this already diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt new file mode 100644 index 0000000000..61432a7b12 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt @@ -0,0 +1,66 @@ +package software.amazon.smithy.kotlin.codegen.rendering.checksums + +import software.amazon.smithy.aws.traits.HttpChecksumTrait +import software.amazon.smithy.kotlin.codegen.KotlinSettings +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.model.hasTrait +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait + +/** + * Handles the `httpChecksumRequired` trait. + * See: https://smithy.io/2.0/spec/http-bindings.html#httpchecksumrequired-trait + */ +class HttpChecksumRequiredIntegration : KotlinIntegration { + override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = + model.isTraitApplied(HttpChecksumRequiredTrait::class.java) + + override fun customizeMiddleware( + ctx: ProtocolGenerator.GenerationContext, + resolved: List, + ): List = resolved + httpChecksumRequiredDefaultAlgorithmMiddleware + httpChecksumRequiredMiddleware +} + +/** + * Adds default checksum algorithm to the execution context + */ +private val httpChecksumRequiredDefaultAlgorithmMiddleware = object : ProtocolMiddleware { + override val name: String = "httpChecksumRequiredDefaultAlgorithmMiddleware" + override val order: Byte = -2 // Before S3 Express (possibly) changes the default (-1) and before calculating checksum (0) + + override fun isEnabledFor(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean = + op.hasTrait() && !op.hasTrait() + + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.write( + "op.context[#T.DefaultChecksumAlgorithm] = #S", + RuntimeTypes.HttpClient.Operation.HttpOperationContext, + "MD5", + ) + } +} + +/** + * Adds interceptor to calculate request checksums. + * The `httpChecksum` trait supersedes the `httpChecksumRequired` trait. If both are applied to an operation use `httpChecksum`. + * + * See: https://smithy.io/2.0/aws/aws-core.html#behavior-with-httpchecksumrequired + */ +private val httpChecksumRequiredMiddleware = object : ProtocolMiddleware { + override val name: String = "httpChecksumRequiredMiddleware" + + override fun isEnabledFor(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean = + op.hasTrait() && !op.hasTrait() + + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.write( + "op.interceptors.add(#T())", + RuntimeTypes.HttpClient.Interceptors.HttpChecksumRequiredInterceptor, + ) + } +} diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt index 519957cbed..f12ab07a26 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt @@ -4,7 +4,6 @@ */ package software.amazon.smithy.kotlin.codegen.rendering.protocol -import software.amazon.smithy.aws.traits.HttpChecksumTrait import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.integration.SectionId @@ -22,7 +21,6 @@ import software.amazon.smithy.model.knowledge.OperationIndex import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.traits.EndpointTrait -import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait /** * Renders an implementation of a service interface for HTTP protocol @@ -318,8 +316,6 @@ open class HttpProtocolClientGenerator( .forEach { middleware -> middleware.render(ctx, op, writer) } - - op.renderIsMd5ChecksumRequired(writer) } /** @@ -336,27 +332,6 @@ open class HttpProtocolClientGenerator( */ protected open fun renderAdditionalMethods(writer: KotlinWriter) { } - /** - * Render optionally installing Md5ChecksumMiddleware. - * The Md5 middleware will only be installed if the operation requires a checksum and the user has not opted-in to flexible checksums. - */ - private fun OperationShape.renderIsMd5ChecksumRequired(writer: KotlinWriter) { - val httpChecksumTrait = getTrait() - - // the checksum requirement can be modeled in either HttpChecksumTrait's `requestChecksumRequired` or the HttpChecksumRequired trait - if (!hasTrait() && httpChecksumTrait == null) { - return - } - - if (hasTrait() || httpChecksumTrait?.isRequestChecksumRequired == true) { - val interceptorSymbol = RuntimeTypes.HttpClient.Interceptors.Md5ChecksumInterceptor - val inputSymbol = ctx.symbolProvider.toSymbol(ctx.model.expectShape(inputShape)) - writer.withBlock("op.interceptors.add(#T<#T> {", "})", interceptorSymbol, inputSymbol) { - writer.write("op.context.getOrNull(#T.ChecksumAlgorithm) == null", RuntimeTypes.HttpClient.Operation.HttpOperationContext) - } - } - } - /** * render a utility function to populate an operation's ExecutionContext with defaults from service config, environment, etc */ diff --git a/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration b/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration index 0d02b51187..2ab7fe506a 100644 --- a/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +++ b/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration @@ -13,3 +13,4 @@ software.amazon.smithy.kotlin.codegen.rendering.endpoints.SdkEndpointBuiltinInte software.amazon.smithy.kotlin.codegen.rendering.compression.RequestCompressionIntegration software.amazon.smithy.kotlin.codegen.rendering.auth.SigV4AsymmetricAuthSchemeIntegration software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestsIntegration +software.amazon.smithy.kotlin.codegen.rendering.checksums.HttpChecksumRequiredIntegration \ No newline at end of file diff --git a/runtime/protocol/http-client/api/http-client.api b/runtime/protocol/http-client/api/http-client.api index 6e91f528f2..e8527c2c2b 100644 --- a/runtime/protocol/http-client/api/http-client.api +++ b/runtime/protocol/http-client/api/http-client.api @@ -255,7 +255,7 @@ public final class aws/smithy/kotlin/runtime/http/engine/internal/ManagedHttpCli public static final fun manage (Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine;)Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine; } -public abstract class aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { +public abstract class aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public fun ()V public abstract fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest; public abstract fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -331,19 +331,17 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpoin public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } -public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor : aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor { - public fun ()V - public fun (Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor : aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor { + public fun (ZLaws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig;Ljava/lang/String;)V public fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest; public fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } -public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { +public class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public static final field Companion Laws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor$Companion; - public fun (Lkotlin/jvm/functions/Function1;)V + public fun (ZLaws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig;)V + public fun ignoreChecksum (Ljava/lang/String;Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)Z public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -369,10 +367,8 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksums public final fun getChecksumHeaderValidated ()Laws/smithy/kotlin/runtime/collections/AttributeKey; } -public final class aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor : aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor { +public final class aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor : aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor { public fun ()V - public fun (Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest; public fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -516,9 +512,9 @@ public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpDes public final class aws/smithy/kotlin/runtime/http/operation/HttpOperationContext { public static final field INSTANCE Laws/smithy/kotlin/runtime/http/operation/HttpOperationContext; - public final fun getChecksumAlgorithm ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getClockSkew ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getClockSkewApproximateSigningTime ()Laws/smithy/kotlin/runtime/collections/AttributeKey; + public final fun getDefaultChecksumAlgorithm ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getHostPrefix ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getHttpCallList ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getOperationAttributes ()Laws/smithy/kotlin/runtime/collections/AttributeKey; diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor.kt similarity index 67% rename from runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor.kt rename to runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor.kt index 3fa8406bf5..adad89f259 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor.kt @@ -9,13 +9,21 @@ import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.http.request.HttpRequest +/** + * Enables inheriting [HttpInterceptor]s to use checksums caching + */ @InternalApi -public abstract class AbstractChecksumInterceptor : HttpInterceptor { +public abstract class CachingChecksumInterceptor : HttpInterceptor { private var cachedChecksum: String? = null override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { - cachedChecksum ?: calculateChecksum(context).also { cachedChecksum = it } - return cachedChecksum?.let { applyChecksum(context, it) } ?: context.protocolRequest + cachedChecksum = cachedChecksum ?: calculateChecksum(context) + + return if (cachedChecksum != null) { + applyChecksum(context, cachedChecksum!!) + } else { + context.protocolRequest + } } public abstract suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt new file mode 100644 index 0000000000..b510ab14e5 --- /dev/null +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt @@ -0,0 +1,44 @@ +package aws.smithy.kotlin.runtime.http.interceptors + +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.hashing.HashFunction +import aws.smithy.kotlin.runtime.hashing.resolveChecksumAlgorithmHeaderName +import aws.smithy.kotlin.runtime.hashing.toBusinessMetric +import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.http.toCompletingBody +import aws.smithy.kotlin.runtime.http.toHashingBody +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.job + +/** + * Configures [HttpRequest] with AWS chunked streaming to calculate checksum during transmission + * @return [HttpRequest] + */ +internal fun calculateAwsChunkedStreamingChecksum( + context: ProtocolRequestInterceptorContext, + checksumAlgorithm: HashFunction, +): HttpRequest { + val request = context.protocolRequest.toBuilder() + val deferredChecksum = CompletableDeferred(context.executionContext.coroutineContext.job) + val checksumHeader = checksumAlgorithm.resolveChecksumAlgorithmHeaderName() + + request.body = request.body + .toHashingBody(checksumAlgorithm, request.body.contentLength) + .toCompletingBody(deferredChecksum) + + request.headers.append("x-amz-trailer", checksumHeader) + request.trailingHeaders.append(checksumHeader, deferredChecksum) + + context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric()) + + return request.build() +} + +/** + * @return The default checksum algorithm name in the execution context, null if default checksums are disabled. + */ +internal val ProtocolRequestInterceptorContext.defaultChecksumAlgorithmName: String? + get() = executionContext.getOrNull(HttpOperationContext.DefaultChecksumAlgorithm) diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt index b29ede017f..936dead520 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt @@ -5,104 +5,117 @@ package aws.smithy.kotlin.runtime.http.interceptors -import aws.smithy.kotlin.runtime.ClientException import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.client.config.RequestHttpChecksumConfig import aws.smithy.kotlin.runtime.hashing.* import aws.smithy.kotlin.runtime.http.* -import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext import aws.smithy.kotlin.runtime.http.request.HttpRequest -import aws.smithy.kotlin.runtime.http.request.header import aws.smithy.kotlin.runtime.http.request.toBuilder import aws.smithy.kotlin.runtime.io.* +import aws.smithy.kotlin.runtime.telemetry.logging.Logger import aws.smithy.kotlin.runtime.telemetry.logging.logger import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String -import aws.smithy.kotlin.runtime.util.LazyAsyncValue -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.job import kotlin.coroutines.coroutineContext /** - * Mutate a request to enable flexible checksums. + * Handles request checksums for operations with the [HttpChecksumTrait] applied. * - * If the checksum will be sent as a header, calculate the checksum. + * If a user supplies a checksum via an HTTP header no calculation will be done. The exception is MD5, if a user + * supplies an MD5 checksum header it will be ignored. * - * Otherwise, if it will be sent as a trailing header, calculate the checksum as asynchronously as the body is streamed. - * In this case, a [LazyAsyncValue] will be added to the execution context which allows the trailing checksum to be sent - * after the entire body has been streamed. + * If the request configuration and model requires checksum calculation: + * - Check if the user configured a checksum algorithm for the request and attempt to use that. + * - If no checksum is configured for the request then use the default checksum algorithm to calculate a checksum. * - * @param checksumAlgorithmNameInitializer an optional function which parses the input [I] to return the checksum algorithm name. - * if not set, then the [HttpOperationContext.ChecksumAlgorithm] execution context attribute will be used. + * If the request will be streamed: + * - The checksum calculation is done during transmission using a hashing & completing body. + * - The checksum will be sent in a trailing header, once the request is consumed. + * + * If the request will not be streamed: + * - The checksum calculation is done before transmission + * - The checksum will be sent in a header + * + * Business metrics MUST be emitted for the checksum algorithm used. + * + * @param requestChecksumRequired Model sourced flag indicating if checksum calculation is mandatory. + * @param requestChecksumCalculation Configuration option that determines when checksum calculation should be done. + * @param requestChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null. */ @InternalApi -public class FlexibleChecksumsRequestInterceptor( - private val checksumAlgorithmNameInitializer: ((I) -> String?)? = null, -) : AbstractChecksumInterceptor() { - private var checksumAlgorithmName: String? = null - - @Deprecated("readAfterSerialization is no longer used") - override fun readAfterSerialization(context: ProtocolRequestInterceptorContext) { } - +public class FlexibleChecksumsRequestInterceptor( + private val requestChecksumRequired: Boolean, + private val requestChecksumCalculation: RequestHttpChecksumConfig?, + private val requestChecksumAlgorithm: String?, +) : CachingChecksumInterceptor() { override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { - val logger = coroutineContext.logger>() + val logger = coroutineContext.logger() - @Suppress("UNCHECKED_CAST") - val input = context.request as I - checksumAlgorithmName = checksumAlgorithmNameInitializer?.invoke(input) ?: context.executionContext.getOrNull(HttpOperationContext.ChecksumAlgorithm) + context.protocolRequest.userProvidedChecksumHeader(logger)?.let { + logger.debug { "Checksum was supplied via header: skipping checksum calculation" } - checksumAlgorithmName ?: run { - logger.debug { "no checksum algorithm specified, skipping flexible checksums processing" } + val request = context.protocolRequest.toBuilder() + request.headers.removeAllChecksumHeadersExcept(it) return context.protocolRequest } - val req = context.protocolRequest.toBuilder() - - check(context.protocolRequest.body !is HttpBody.Empty) { - "Can't calculate the checksum of an empty body" - } - - val headerName = "x-amz-checksum-$checksumAlgorithmName".lowercase() - logger.debug { "Resolved checksum header name: $headerName" } - - // remove all checksum headers except for $headerName - // this handles the case where a user inputs a precalculated checksum, but it doesn't match the input checksum algorithm - req.headers.removeAllChecksumHeadersExcept(headerName) - - val checksumAlgorithm = checksumAlgorithmName?.toHashFunction() ?: throw ClientException("Could not parse checksum algorithm $checksumAlgorithmName") - - if (!checksumAlgorithm.isSupported) { - throw ClientException("Checksum algorithm $checksumAlgorithmName is not supported for flexible checksums") - } - - if (req.body.isEligibleForAwsChunkedStreaming) { - req.header("x-amz-trailer", headerName) - - val deferredChecksum = CompletableDeferred(context.executionContext.coroutineContext.job) - - if (req.headers[headerName] != null) { - logger.debug { "User supplied a checksum, skipping asynchronous calculation" } - - val checksum = req.headers[headerName]!! - req.headers.remove(headerName) // remove the checksum header because it will be sent as a trailing header - - deferredChecksum.complete(checksum) + resolveChecksumAlgorithm( + requestChecksumRequired, + requestChecksumCalculation, + requestChecksumAlgorithm, + context, + )?.let { checksumAlgorithm -> + return if (context.protocolRequest.body.isEligibleForAwsChunkedStreaming) { + logger.debug { "Calculating checksum during transmission using: ${checksumAlgorithm::class.simpleName}" } + calculateAwsChunkedStreamingChecksum(context, checksumAlgorithm) } else { - logger.debug { "Calculating checksum asynchronously" } - req.body = req.body - .toHashingBody(checksumAlgorithm, req.body.contentLength) - .toCompletingBody(deferredChecksum) + if (context.protocolRequest.body is HttpBody.Bytes) { + // Cache checksum + super.modifyBeforeSigning(context) + } else { + val checksum = calculateFlexibleChecksumsChecksum(context) + applyFlexibleChecksumsChecksum(context, checksum) + } } - - req.trailingHeaders.append(headerName, deferredChecksum) - return req.build() - } else { - return super.modifyBeforeSigning(context) } + + logger.debug { "Checksum wasn't provided, selected, or isn't required: skipping checksum calculation" } + return context.protocolRequest } - override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? { + /** + * Determines what checksum algorithm to use, null if none is required + */ + private fun resolveChecksumAlgorithm( + requestChecksumRequired: Boolean, + requestChecksumCalculation: RequestHttpChecksumConfig?, + requestChecksumAlgorithm: String?, + context: ProtocolRequestInterceptorContext, + ): HashFunction? = + requestChecksumAlgorithm + ?.toHashFunctionOrThrow() + ?.takeIf { it.isSupportedForFlexibleChecksums } + ?: context.defaultChecksumAlgorithmName + ?.toHashFunctionOrThrow() + ?.takeIf { + (requestChecksumRequired || requestChecksumCalculation == RequestHttpChecksumConfig.WHEN_SUPPORTED) && + it.isSupportedForFlexibleChecksums + } + + /** + * Calculates a checksum based on the requirements and limitations of [FlexibleChecksumsRequestInterceptor] + */ + private suspend fun calculateFlexibleChecksumsChecksum( + context: ProtocolRequestInterceptorContext, + ): String { val req = context.protocolRequest.toBuilder() - val checksumAlgorithm = checksumAlgorithmName?.toHashFunction() ?: return null + val checksumAlgorithm = resolveChecksumAlgorithm( + requestChecksumRequired, + requestChecksumCalculation, + requestChecksumAlgorithm, + context, + )!! return when { req.body.contentLength == null && !req.body.isOneShot -> { @@ -110,104 +123,66 @@ public class FlexibleChecksumsRequestInterceptor( channel.rollingHash(checksumAlgorithm).encodeBase64String() } else -> { - val bodyBytes = req.body.readAll()!! - req.body = bodyBytes.toHttpBody() + val bodyBytes = req.body.readAll() ?: byteArrayOf() + if (req.body.isOneShot) req.body = bodyBytes.toHttpBody() bodyBytes.hash(checksumAlgorithm).encodeBase64String() } } } - override fun applyChecksum( + override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? = + calculateFlexibleChecksumsChecksum(context) + + /** + * Applies a checksum based on the requirements and limitations of [FlexibleChecksumsRequestInterceptor] + */ + private fun applyFlexibleChecksumsChecksum( context: ProtocolRequestInterceptorContext, checksum: String, ): HttpRequest { - val headerName = "x-amz-checksum-$checksumAlgorithmName".lowercase() - - val req = context.protocolRequest.toBuilder() - - if (!req.headers.contains(headerName)) { - req.header(headerName, checksum) - } - - return req.build() + val request = context.protocolRequest.toBuilder() + val checksumAlgorithm = resolveChecksumAlgorithm( + requestChecksumRequired, + requestChecksumCalculation, + requestChecksumAlgorithm, + context, + )!! + val checksumHeader = checksumAlgorithm.resolveChecksumAlgorithmHeaderName() + + request.headers[checksumHeader] = checksum + request.headers.removeAllChecksumHeadersExcept(checksumHeader) + context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric()) + + return request.build() } - // FIXME this duplicates the logic from aws-signing-common, but can't import from there due to circular import. - private val HttpBody.isEligibleForAwsChunkedStreaming: Boolean - get() = (this is HttpBody.SourceContent || this is HttpBody.ChannelContent) && - contentLength != null && - (isOneShot || contentLength!! > 65536 * 16) + override fun applyChecksum( + context: ProtocolRequestInterceptorContext, + checksum: String, + ): HttpRequest = applyFlexibleChecksumsChecksum(context, checksum) /** - * @return if the [HashFunction] is supported by flexible checksums + * Checks if a user provided a checksum for a request via an HTTP header. + * The header must start with "x-amz-checksum-" followed by the checksum algorithm's name. + * MD5 is not considered a supported checksum algorithm. */ - private val HashFunction.isSupported: Boolean get() = when (this) { - is Crc32, is Crc32c, is Sha256, is Sha1 -> true - else -> false - } + private fun HttpRequest.userProvidedChecksumHeader(logger: Logger) = headers + .names() + .firstOrNull { + it.startsWith("x-amz-checksum-", ignoreCase = true) && + !it.equals("x-amz-checksum-md5", ignoreCase = true).also { isMd5 -> + if (isMd5) { + logger.debug { "MD5 checksum was supplied via header, MD5 is not a supported algorithm, ignoring header" } + } + } + } /** * Removes all checksum headers except [headerName] * @param headerName the checksum header name to keep */ - private fun HeadersBuilder.removeAllChecksumHeadersExcept(headerName: String) { - names().forEach { name -> - if (name.startsWith("x-amz-checksum-") && name != headerName) { - remove(name) - } - } - } - - /** - * Convert an [HttpBody] with an underlying [HashingSource] or [HashingByteReadChannel] - * to a [CompletingSource] or [CompletingByteReadChannel], respectively. - */ - private fun HttpBody.toCompletingBody(deferred: CompletableDeferred) = when (this) { - is HttpBody.SourceContent -> CompletingSource(deferred, (readFrom() as HashingSource)).toHttpBody(contentLength) - is HttpBody.ChannelContent -> CompletingByteReadChannel(deferred, (readFrom() as HashingByteReadChannel)).toHttpBody(contentLength) - else -> throw ClientException("HttpBody type is not supported") - } - - /** - * An [SdkSource] which uses the underlying [hashingSource]'s checksum to complete a [CompletableDeferred] value. - */ - internal class CompletingSource( - private val deferred: CompletableDeferred, - private val hashingSource: HashingSource, - ) : SdkSource by hashingSource { - override fun read(sink: SdkBuffer, limit: Long): Long = hashingSource.read(sink, limit) - .also { - if (it == -1L) { - deferred.complete(hashingSource.digest().encodeBase64String()) - } - } - } - - /** - * An [SdkByteReadChannel] which uses the underlying [hashingChannel]'s checksum to complete a [CompletableDeferred] value. - */ - internal class CompletingByteReadChannel( - private val deferred: CompletableDeferred, - private val hashingChannel: HashingByteReadChannel, - ) : SdkByteReadChannel by hashingChannel { - override suspend fun read(sink: SdkBuffer, limit: Long): Long = hashingChannel.read(sink, limit) - .also { - if (it == -1L) { - deferred.complete(hashingChannel.digest().encodeBase64String()) - } - } - } - - /** - * Compute the rolling hash of an [SdkByteReadChannel] using [hashFunction], reading up-to [bufferSize] bytes into memory - * @return a ByteArray of the hash function's digest - */ - private suspend fun SdkByteReadChannel.rollingHash(hashFunction: HashFunction, bufferSize: Long = 8192): ByteArray { - val buffer = SdkBuffer() - while (!isClosedForRead) { - read(buffer, bufferSize) - hashFunction.update(buffer.readToByteArray()) - } - return hashFunction.digest() - } + private fun HeadersBuilder.removeAllChecksumHeadersExcept(headerName: String) = + names() + .filter { it.startsWith("x-amz-checksum-", ignoreCase = true) && !it.equals(headerName, ignoreCase = true) } + .forEach { remove(it) } } diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt index 2e43fe1469..eceb19e739 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt @@ -8,10 +8,11 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.ClientException import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.client.ProtocolResponseInterceptorContext -import aws.smithy.kotlin.runtime.client.RequestInterceptorContext +import aws.smithy.kotlin.runtime.client.config.ResponseHttpChecksumConfig import aws.smithy.kotlin.runtime.collections.AttributeKey -import aws.smithy.kotlin.runtime.hashing.toHashFunction +import aws.smithy.kotlin.runtime.hashing.toHashFunctionOrThrow import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.readAll import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.http.response.HttpResponse import aws.smithy.kotlin.runtime.http.response.copy @@ -31,60 +32,87 @@ internal val CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST: List = listOf( ) /** - * Validate a response's checksum. + * Handles response checksums. + * + * If it's a streaming response, it wraps the response in a hashing body, calculating the checksum as the response is + * streamed to the user. The checksum is validated after the user has consumed the entire body using a checksum validating body. + * Otherwise, the checksum if calculated all at once. * - * Wraps the response in a hashing body, calculating the checksum as the response is streamed to the user. - * The checksum is validated after the user has consumed the entire body using a checksum validating body. * Users can check which checksum was validated by referencing the `ResponseChecksumValidated` execution context variable. * - * @param shouldValidateResponseChecksumInitializer A function which uses the input [I] to return whether response checksum validation should occur + * @param responseValidationRequired Model sourced flag indicating if the checksum validation is mandatory. + * @param responseChecksumValidation Configuration option that determines when checksum validation should be done. */ - @InternalApi -public class FlexibleChecksumsResponseInterceptor( - private val shouldValidateResponseChecksumInitializer: (input: I) -> Boolean, +public open class FlexibleChecksumsResponseInterceptor( + private val responseValidationRequired: Boolean, + private val responseChecksumValidation: ResponseHttpChecksumConfig?, ) : HttpInterceptor { - - private var shouldValidateResponseChecksum: Boolean = false - @InternalApi public companion object { // The name of the checksum header which was validated. If `null`, validation was not performed. public val ChecksumHeaderValidated: AttributeKey = AttributeKey("ChecksumHeaderValidated") } - override fun readBeforeSerialization(context: RequestInterceptorContext) { - @Suppress("UNCHECKED_CAST") - val input = context.request as I - shouldValidateResponseChecksum = shouldValidateResponseChecksumInitializer(input) - } - override suspend fun modifyBeforeDeserialization(context: ProtocolResponseInterceptorContext): HttpResponse { - if (!shouldValidateResponseChecksum) { - return context.protocolResponse - } + val configuredToVerifyChecksum = responseValidationRequired || responseChecksumValidation == ResponseHttpChecksumConfig.WHEN_SUPPORTED + if (!configuredToVerifyChecksum) return context.protocolResponse - val logger = coroutineContext.logger>() + val logger = coroutineContext.logger() val checksumHeader = CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST .firstOrNull { context.protocolResponse.headers.contains(it) } ?: run { - logger.warn { "User requested checksum validation, but the response headers did not contain any valid checksums" } + logger.warn { "Checksum validation was requested but the response headers didn't contain a valid checksum." } return context.protocolResponse } - // let the user know which checksum will be validated - logger.debug { "Validating checksum from $checksumHeader" } - context.executionContext[ChecksumHeaderValidated] = checksumHeader + val serviceChecksumValue = context.protocolResponse.headers[checksumHeader]!! + if (ignoreChecksum(serviceChecksumValue, context)) { + return context.protocolResponse + } - val checksumAlgorithm = checksumHeader.removePrefix("x-amz-checksum-").toHashFunction() ?: throw ClientException("could not parse checksum algorithm from header $checksumHeader") + context.executionContext[ChecksumHeaderValidated] = checksumHeader - // Wrap the response body in a hashing body - return context.protocolResponse.copy( - body = context.protocolResponse.body - .toHashingBody(checksumAlgorithm, context.protocolResponse.body.contentLength) - .toChecksumValidatingBody(context.protocolResponse.headers[checksumHeader]!!), - ) + val checksumAlgorithm = checksumHeader + .removePrefix("x-amz-checksum-") + .toHashFunctionOrThrow() + + when (val bodyType = context.protocolResponse.body) { + is HttpBody.Bytes -> { + logger.debug { "Validating checksum before deserialization from $checksumHeader" } + + checksumAlgorithm.update( + context.protocolResponse.body.readAll() ?: byteArrayOf(), + ) + val sdkChecksumValue = checksumAlgorithm.digest().encodeBase64String() + + validateAndThrow( + serviceChecksumValue, + sdkChecksumValue, + ) + + return context.protocolResponse + } + is HttpBody.SourceContent, is HttpBody.ChannelContent -> { + logger.debug { "Validating checksum after deserialization from $checksumHeader" } + + return context.protocolResponse.copy( + body = context.protocolResponse.body + .toHashingBody(checksumAlgorithm, context.protocolResponse.body.contentLength) + .toChecksumValidatingBody(serviceChecksumValue), + ) + } + else -> throw IllegalStateException("HTTP body type '$bodyType' is not supported for flexible checksums.") + } } + + /** + * Additional check on the checksum itself to see if it should be validated + */ + public open fun ignoreChecksum( + checksum: String, + context: ProtocolResponseInterceptorContext, + ): Boolean = false } public class ChecksumMismatchException(message: String?) : ClientException(message) diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt new file mode 100644 index 0000000000..f422de7fa7 --- /dev/null +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.http.interceptors + +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.hashing.* +import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.header +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.io.rollingHash +import aws.smithy.kotlin.runtime.telemetry.logging.logger +import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String +import kotlin.coroutines.coroutineContext + +/** + * Handles checksum request calculation from the `httpChecksumRequired` trait. + */ +@InternalApi +public class HttpChecksumRequiredInterceptor : CachingChecksumInterceptor() { + override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { + if (context.defaultChecksumAlgorithmName == null) { + // Don't calculate checksum + return context.protocolRequest + } + + val checksumAlgorithmName = context.defaultChecksumAlgorithmName!! + val checksumAlgorithm = checksumAlgorithmName.toHashFunctionOrThrow() + + return if (context.protocolRequest.body.isEligibleForAwsChunkedStreaming) { + coroutineContext.logger().debug { + "Calculating checksum during transmission using: ${checksumAlgorithm::class.simpleName}" + } + calculateAwsChunkedStreamingChecksum(context, checksumAlgorithm) + } else { + if (context.protocolRequest.body is HttpBody.Bytes) { + // Cache checksum + super.modifyBeforeSigning(context) + } else { + val checksum = calculateHttpChecksumRequiredChecksum(context) + applyHttpChecksumRequiredChecksum(context, checksum) + } + } + } + + /** + * Calculates a checksum based on the requirements and limitations of [HttpChecksumRequiredInterceptor] + */ + private suspend fun calculateHttpChecksumRequiredChecksum( + context: ProtocolRequestInterceptorContext, + ): String { + val req = context.protocolRequest.toBuilder() + val checksumAlgorithmName = context.defaultChecksumAlgorithmName!! + val checksumAlgorithm = checksumAlgorithmName.toHashFunctionOrThrow() + + return when { + req.body.contentLength == null && !req.body.isOneShot -> { + val channel = req.body.toSdkByteReadChannel()!! + channel.rollingHash(checksumAlgorithm).encodeBase64String() + } + else -> { + val bodyBytes = req.body.readAll() ?: byteArrayOf() + if (req.body.isOneShot) req.body = bodyBytes.toHttpBody() + bodyBytes.hash(checksumAlgorithm).encodeBase64String() + } + } + } + + public override suspend fun calculateChecksum( + context: ProtocolRequestInterceptorContext, + ): String? = + calculateHttpChecksumRequiredChecksum(context) + + /** + * Applies a checksum based on the requirements and limitations of [HttpChecksumRequiredInterceptor] + */ + private fun applyHttpChecksumRequiredChecksum( + context: ProtocolRequestInterceptorContext, + checksum: String, + ): HttpRequest { + val checksumAlgorithmName = context.defaultChecksumAlgorithmName!! + val checksumHeader = checksumAlgorithmName.resolveChecksumAlgorithmHeaderName() + val request = context.protocolRequest.toBuilder() + + request.header(checksumHeader, checksum) + return request.build() + } + + public override fun applyChecksum( + context: ProtocolRequestInterceptorContext, + checksum: String, + ): HttpRequest = + applyHttpChecksumRequiredChecksum(context, checksum) +} diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt deleted file mode 100644 index cbe6bcabe2..0000000000 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package aws.smithy.kotlin.runtime.http.interceptors - -import aws.smithy.kotlin.runtime.InternalApi -import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext -import aws.smithy.kotlin.runtime.hashing.md5 -import aws.smithy.kotlin.runtime.http.HttpBody -import aws.smithy.kotlin.runtime.http.request.HttpRequest -import aws.smithy.kotlin.runtime.http.request.header -import aws.smithy.kotlin.runtime.http.request.toBuilder -import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String - -/** - * Set the `Content-MD5` header based on the current payload - * See: - * - https://awslabs.github.io/smithy/1.0/spec/core/behavior-traits.html#httpchecksumrequired-trait - * - https://datatracker.ietf.org/doc/html/rfc1864.html - * @param block An optional function which parses the input [I] to determine if the `Content-MD5` header should be set. - * If not provided, the default behavior will set the header. - */ -@InternalApi -public class Md5ChecksumInterceptor( - private val block: ((input: I) -> Boolean)? = null, -) : AbstractChecksumInterceptor() { - override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { - @Suppress("UNCHECKED_CAST") - val input = context.request as I - - val injectMd5Header = block?.invoke(input) ?: true - if (!injectMd5Header) { - return context.protocolRequest - } - - return super.modifyBeforeSigning(context) - } - - public override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? = - when (val body = context.protocolRequest.body) { - is HttpBody.Bytes -> body.bytes().md5().encodeBase64String() - else -> null - } - - public override fun applyChecksum(context: ProtocolRequestInterceptorContext, checksum: String): HttpRequest { - val req = context.protocolRequest.toBuilder() - if (!req.headers.contains("Content-MD5")) { - req.header("Content-MD5", checksum) - } - return req.build() - } -} diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt index 524614a667..56444dca98 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt @@ -66,9 +66,9 @@ public object HttpOperationContext { public val ClockSkewApproximateSigningTime: AttributeKey = AttributeKey("aws.smithy.kotlin#ClockSkewApproximateSigningTime") /** - * The name of the algorithm to be used for computing a checksum of the request. + * The name of the default algorithm to be used for computing a checksum of the request. */ - public val ChecksumAlgorithm: AttributeKey = AttributeKey("aws.smithy.kotlin#ChecksumAlgorithm") + public val DefaultChecksumAlgorithm: AttributeKey = AttributeKey("aws.smithy.kotlin#DefaultChecksumAlgorithm") } internal val ExecutionContext.operationMetrics: OperationMetrics diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptorTest.kt similarity index 88% rename from runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptorTest.kt rename to runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptorTest.kt index 3de8e557c2..cee1db7766 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptorTest.kt @@ -6,7 +6,7 @@ import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.SdkHttpClient -import aws.smithy.kotlin.runtime.http.interceptors.AbstractChecksumInterceptor +import aws.smithy.kotlin.runtime.http.interceptors.CachingChecksumInterceptor import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext import aws.smithy.kotlin.runtime.http.operation.newTestOperation import aws.smithy.kotlin.runtime.http.operation.roundTrip @@ -21,7 +21,7 @@ import kotlin.test.assertEquals private val CHECKSUM_TEST_HEADER = "x-amz-kotlin-sdk-test-checksum-header" -class AbstractChecksumInterceptorTest { +class CachingChecksumInterceptorTest { private val client = SdkHttpClient(TestEngine()) @Test @@ -33,7 +33,7 @@ class AbstractChecksumInterceptorTest { val op = newTestOperation(req, Unit) - op.interceptors.add(TestAbstractChecksumInterceptor(expectedChecksumValue)) + op.interceptors.add(TestCachingChecksumInterceptor(expectedChecksumValue)) op.roundTrip(client, Unit) val call = op.context.attributes[HttpOperationContext.HttpCallList].first() @@ -49,16 +49,16 @@ class AbstractChecksumInterceptorTest { val op = newTestOperation(req, Unit) - op.interceptors.add(TestAbstractChecksumInterceptor(expectedChecksumValue)) + op.interceptors.add(TestCachingChecksumInterceptor(expectedChecksumValue)) // the TestAbstractChecksumInterceptor will throw an exception if calculateChecksum is called more than once. op.roundTrip(client, Unit) op.roundTrip(client, Unit) } - inner class TestAbstractChecksumInterceptor( + inner class TestCachingChecksumInterceptor( private val expectedChecksum: String?, - ) : AbstractChecksumInterceptor() { + ) : CachingChecksumInterceptor() { private var alreadyCalculatedChecksum = false override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? { diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt index c4c85de66b..37e5054865 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.ClientException +import aws.smithy.kotlin.runtime.client.config.RequestHttpChecksumConfig import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.hashing.toHashFunction import aws.smithy.kotlin.runtime.http.* @@ -41,9 +42,11 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -65,10 +68,13 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "CRC32" op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -87,13 +93,14 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) - op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - unsupportedChecksumAlgorithmName - }, - ) - assertFailsWith { + op.interceptors.add( + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = unsupportedChecksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), + ) op.roundTrip(client, Unit) } } @@ -115,9 +122,11 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -126,23 +135,6 @@ class FlexibleChecksumsRequestInterceptorTest { assertEquals(0, call.request.headers.getNumChecksumHeaders()) } - @Test - fun itSetsChecksumHeaderViaExecutionContext() = runTest { - checksums.forEach { (checksumAlgorithmName, expectedChecksumValue) -> - val req = HttpRequestBuilder().apply { - body = HttpBody.fromBytes("bar".encodeToByteArray()) - } - - val op = newTestOperation(req, Unit) - op.context[HttpOperationContext.ChecksumAlgorithm] = checksumAlgorithmName - op.interceptors.add(FlexibleChecksumsRequestInterceptor()) - - op.roundTrip(client, Unit) - val call = op.context.attributes[HttpOperationContext.HttpCallList].first() - assertEquals(expectedChecksumValue, call.request.headers["x-amz-checksum-$checksumAlgorithmName"]) - } - } - @Test fun testCompletingSource() = runTest { val hashFunctionName = "crc32" @@ -151,7 +143,7 @@ class FlexibleChecksumsRequestInterceptorTest { val source = byteArray.source() val completableDeferred = CompletableDeferred() val hashingSource = HashingSource(hashFunctionName.toHashFunction()!!, source) - val completingSource = FlexibleChecksumsRequestInterceptor.CompletingSource(completableDeferred, hashingSource) + val completingSource = CompletingSource(completableDeferred, hashingSource) completingSource.read(SdkBuffer(), 1L) assertFalse(completableDeferred.isCompleted) // deferred value should not be completed because the source is not exhausted @@ -172,7 +164,8 @@ class FlexibleChecksumsRequestInterceptorTest { val channel = SdkByteReadChannel(byteArray) val completableDeferred = CompletableDeferred() val hashingChannel = HashingByteReadChannel(hashFunctionName.toHashFunction()!!, channel) - val completingChannel = FlexibleChecksumsRequestInterceptor.CompletingByteReadChannel(completableDeferred, hashingChannel) + val completingChannel = + CompletingByteReadChannel(completableDeferred, hashingChannel) completingChannel.read(SdkBuffer(), 1L) assertFalse(completableDeferred.isCompleted) @@ -198,9 +191,11 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -210,5 +205,51 @@ class FlexibleChecksumsRequestInterceptorTest { assertEquals(precalculatedChecksumValue, call.request.headers["x-amz-checksum-sha256"]) } + @Test + fun testDefaultChecksumConfiguration() = runTest { + setOf( + DefaultChecksumTest(true, RequestHttpChecksumConfig.WHEN_SUPPORTED, true), + DefaultChecksumTest(true, RequestHttpChecksumConfig.WHEN_REQUIRED, true), + DefaultChecksumTest(false, RequestHttpChecksumConfig.WHEN_SUPPORTED, true), + DefaultChecksumTest(false, RequestHttpChecksumConfig.WHEN_REQUIRED, false), + ).forEach { runDefaultChecksumTest(it) } + } + private fun Headers.getNumChecksumHeaders(): Int = entries().count { (name, _) -> name.startsWith("x-amz-checksum-") } + + private data class DefaultChecksumTest( + val requestChecksumRequired: Boolean, + val requestChecksumCalculation: RequestHttpChecksumConfig, + val defaultChecksumExpected: Boolean, + ) + + private fun runDefaultChecksumTest( + testCase: DefaultChecksumTest, + ) = runTest { + val defaultChecksumAlgorithmName = "crc32" + val expectedChecksumValue = "WdqXHQ==" + + val req = HttpRequestBuilder().apply { + body = HttpBody.fromBytes("bar".encodeToByteArray()) + } + + val op = newTestOperation(req, Unit) + + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "CRC32" + op.interceptors.add( + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = null, // See if default checksum is applied + requestChecksumRequired = testCase.requestChecksumRequired, + requestChecksumCalculation = testCase.requestChecksumCalculation, + ), + ) + + op.roundTrip(client, Unit) + val call = op.context.attributes[HttpOperationContext.HttpCallList].first() + + when (testCase.defaultChecksumExpected) { + true -> assertEquals(expectedChecksumValue, call.request.headers["x-amz-checksum-$defaultChecksumAlgorithmName"]) + false -> assertFalse { call.request.headers.contains("x-amz-checksum-$defaultChecksumAlgorithmName") } + } + } } diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt index 2c04ee680d..e465499ad1 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.http.interceptors +import aws.smithy.kotlin.runtime.client.config.ResponseHttpChecksumConfig import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.http.* import aws.smithy.kotlin.runtime.http.HttpCall @@ -73,9 +74,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseChecksumHeaderName = "x-amz-checksum-$checksumAlgorithmName" @@ -99,9 +101,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseChecksumHeaderName = "x-amz-checksum-$checksumAlgorithmName" @@ -126,9 +129,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseHeaders = Headers { @@ -150,9 +154,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseHeaders = Headers { @@ -170,9 +175,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - false - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = false, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_REQUIRED, + ), ) val responseChecksumHeaderName = "x-amz-checksum-crc32" @@ -188,4 +194,52 @@ class FlexibleChecksumsResponseInterceptorTest { assertNull(op.context.getOrNull(ChecksumHeaderValidated)) } + + @Test + fun testResponseValidationConfiguration() = runTest { + setOf( + ResponseChecksumValidationTest(true, ResponseHttpChecksumConfig.WHEN_SUPPORTED, true), + ResponseChecksumValidationTest(true, ResponseHttpChecksumConfig.WHEN_REQUIRED, true), + ResponseChecksumValidationTest(false, ResponseHttpChecksumConfig.WHEN_SUPPORTED, true), + ResponseChecksumValidationTest(false, ResponseHttpChecksumConfig.WHEN_REQUIRED, false), + ).forEach { runResponseChecksumValidationTest(it) } + } + + private data class ResponseChecksumValidationTest( + val responseValidationRequired: Boolean, + val responseChecksumValidation: ResponseHttpChecksumConfig, + val checksumValidationExpected: Boolean, + ) + + private fun runResponseChecksumValidationTest( + testCase: ResponseChecksumValidationTest, + ) = runTest { + checksums.forEach { (checksumAlgorithmName, expectedChecksum) -> + val req = HttpRequestBuilder() + val op = newTestOperation(req) + + op.interceptors.add( + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = testCase.responseValidationRequired, + responseChecksumValidation = testCase.responseChecksumValidation, + ), + ) + + val responseChecksumHeaderName = "x-amz-checksum-$checksumAlgorithmName" + + val responseHeaders = Headers { + append(responseChecksumHeaderName, expectedChecksum) + } + + val client = getMockClient(response, responseHeaders) + + val output = op.roundTrip(client, TestInput("input")) + output.body.readAll() + + when (testCase.checksumValidationExpected) { + true -> assertEquals(responseChecksumHeaderName, op.context[ChecksumHeaderValidated]) + false -> assertNull(op.context.getOrNull(ChecksumHeaderValidated)) + } + } + } } diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptorTest.kt similarity index 62% rename from runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptorTest.kt rename to runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptorTest.kt index 109d2e3e8d..df2cf49f0e 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptorTest.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.collections.get +import aws.smithy.kotlin.runtime.hashing.Crc32 import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.SdkHttpClient import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext @@ -14,12 +15,13 @@ import aws.smithy.kotlin.runtime.http.operation.roundTrip import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder import aws.smithy.kotlin.runtime.httptest.TestEngine import aws.smithy.kotlin.runtime.io.SdkByteReadChannel +import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull -class Md5ChecksumInterceptorTest { +class HttpChecksumRequiredInterceptorTest { private val client = SdkHttpClient(TestEngine()) @Test @@ -29,10 +31,9 @@ class Md5ChecksumInterceptorTest { } val op = newTestOperation(req, Unit) + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "MD5" op.interceptors.add( - Md5ChecksumInterceptor { - true - }, + HttpChecksumRequiredInterceptor(), ) val expected = "RG22oBSZFmabBbkzVGRi4w==" @@ -42,7 +43,30 @@ class Md5ChecksumInterceptorTest { } @Test - fun itOnlySetsHeaderForBytesContent() = runTest { + fun itSetsContentCrc32Header() = runTest { + val testBody = "bar".encodeToByteArray() + + val req = HttpRequestBuilder().apply { + body = HttpBody.fromBytes(testBody) + } + val op = newTestOperation(req, Unit) + + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "CRC32" + op.interceptors.add( + HttpChecksumRequiredInterceptor(), + ) + + val crc32 = Crc32() + crc32.update(testBody) + val expected = crc32.digest().encodeBase64String() + + op.roundTrip(client, Unit) + val call = op.context.attributes[HttpOperationContext.HttpCallList].first() + assertEquals(expected, call.request.headers["x-amz-checksum-crc32"]) + } + + @Test + fun itSetsHeaderForNonBytesContent() = runTest { val req = HttpRequestBuilder().apply { body = object : HttpBody.ChannelContent() { override fun readFrom(): SdkByteReadChannel = SdkByteReadChannel("fooey".encodeToByteArray()) @@ -50,15 +74,15 @@ class Md5ChecksumInterceptorTest { } val op = newTestOperation(req, Unit) + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "MD5" op.interceptors.add( - Md5ChecksumInterceptor { - true - }, + HttpChecksumRequiredInterceptor(), ) + val expected = "vJLiaOiNxaxdWfYAYzdzFQ==" op.roundTrip(client, Unit) val call = op.context.attributes[HttpOperationContext.HttpCallList].first() - assertNull(call.request.headers["Content-MD5"]) + assertEquals(expected, call.request.headers["Content-MD5"]) } @Test @@ -69,9 +93,7 @@ class Md5ChecksumInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - Md5ChecksumInterceptor { - false // interceptor disabled - }, + HttpChecksumRequiredInterceptor(), ) op.roundTrip(client, Unit) diff --git a/runtime/protocol/http/api/http.api b/runtime/protocol/http/api/http.api index 3f4c29414f..fb338e64d9 100644 --- a/runtime/protocol/http/api/http.api +++ b/runtime/protocol/http/api/http.api @@ -1,3 +1,19 @@ +public final class aws/smithy/kotlin/runtime/http/CompletingByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel { + public fun (Lkotlinx/coroutines/CompletableDeferred;Laws/smithy/kotlin/runtime/io/HashingByteReadChannel;)V + public fun cancel (Ljava/lang/Throwable;)Z + public fun getAvailableForRead ()I + public fun getClosedCause ()Ljava/lang/Throwable; + public fun isClosedForRead ()Z + public fun isClosedForWrite ()Z + public fun read (Laws/smithy/kotlin/runtime/io/SdkBuffer;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class aws/smithy/kotlin/runtime/http/CompletingSource : aws/smithy/kotlin/runtime/io/SdkSource { + public fun (Lkotlinx/coroutines/CompletableDeferred;Laws/smithy/kotlin/runtime/io/HashingSource;)V + public fun close ()V + public fun read (Laws/smithy/kotlin/runtime/io/SdkBuffer;J)J +} + public abstract interface class aws/smithy/kotlin/runtime/http/DeferredHeaders : aws/smithy/kotlin/runtime/collections/ValuesMap { public static final field Companion Laws/smithy/kotlin/runtime/http/DeferredHeaders$Companion; } @@ -86,8 +102,10 @@ public abstract class aws/smithy/kotlin/runtime/http/HttpBody$SourceContent : aw } public final class aws/smithy/kotlin/runtime/http/HttpBodyKt { + public static final fun isEligibleForAwsChunkedStreaming (Laws/smithy/kotlin/runtime/http/HttpBody;)Z public static final fun readAll (Laws/smithy/kotlin/runtime/http/HttpBody;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun toByteStream (Laws/smithy/kotlin/runtime/http/HttpBody;)Laws/smithy/kotlin/runtime/content/ByteStream; + public static final fun toCompletingBody (Laws/smithy/kotlin/runtime/http/HttpBody;Lkotlinx/coroutines/CompletableDeferred;)Laws/smithy/kotlin/runtime/http/HttpBody; public static final fun toHashingBody (Laws/smithy/kotlin/runtime/http/HttpBody;Laws/smithy/kotlin/runtime/hashing/HashFunction;Ljava/lang/Long;)Laws/smithy/kotlin/runtime/http/HttpBody; public static final fun toHttpBody (Laws/smithy/kotlin/runtime/content/ByteStream;)Laws/smithy/kotlin/runtime/http/HttpBody; public static final fun toHttpBody (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Ljava/lang/Long;)Laws/smithy/kotlin/runtime/http/HttpBody; diff --git a/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt b/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt index b86c5c2199..a12626a656 100644 --- a/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt +++ b/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt @@ -10,6 +10,8 @@ import aws.smithy.kotlin.runtime.content.ByteStream import aws.smithy.kotlin.runtime.hashing.HashFunction import aws.smithy.kotlin.runtime.http.content.ByteArrayContent import aws.smithy.kotlin.runtime.io.* +import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope /** @@ -191,6 +193,49 @@ public fun HttpBody.toHashingBody( else -> throw ClientException("HttpBody type is not supported") } +/** + * Convert an [HttpBody] with an underlying [HashingSource] or [HashingByteReadChannel] + * to a [CompletingSource] or [CompletingByteReadChannel], respectively. + */ +@InternalApi +public fun HttpBody.toCompletingBody(deferred: CompletableDeferred): HttpBody = when (this) { + is HttpBody.SourceContent -> CompletingSource(deferred, (readFrom() as HashingSource)).toHttpBody(contentLength) + is HttpBody.ChannelContent -> CompletingByteReadChannel(deferred, (readFrom() as HashingByteReadChannel)).toHttpBody(contentLength) + else -> throw ClientException("HttpBody type is not supported") +} + +/** + * An [SdkSource] which uses the underlying [hashingSource]'s checksum to complete a [CompletableDeferred] value. + */ +@InternalApi +public class CompletingSource( + private val deferred: CompletableDeferred, + private val hashingSource: HashingSource, +) : SdkSource by hashingSource { + override fun read(sink: SdkBuffer, limit: Long): Long = hashingSource.read(sink, limit) + .also { + if (it == -1L) { + deferred.complete(hashingSource.digest().encodeBase64String()) + } + } +} + +/** + * An [SdkByteReadChannel] which uses the underlying [hashingChannel]'s checksum to complete a [CompletableDeferred] value. + */ +@InternalApi +public class CompletingByteReadChannel( + private val deferred: CompletableDeferred, + private val hashingChannel: HashingByteReadChannel, +) : SdkByteReadChannel by hashingChannel { + override suspend fun read(sink: SdkBuffer, limit: Long): Long = hashingChannel.read(sink, limit) + .also { + if (it == -1L) { + deferred.complete(hashingChannel.digest().encodeBase64String()) + } + } +} + // FIXME - replace/move to reading to SdkBuffer instead /** * Consume the [HttpBody] and pull the entire contents into memory as a [ByteArray]. @@ -244,3 +289,10 @@ public fun HttpBody.toSdkByteReadChannel(scope: CoroutineScope? = null): SdkByte is HttpBody.ChannelContent -> body.readFrom() is HttpBody.SourceContent -> body.readFrom().toSdkByteReadChannel(scope) } + +// FIXME this duplicates the logic from aws-signing-common +@InternalApi +public val HttpBody.isEligibleForAwsChunkedStreaming: Boolean + get() = (this is HttpBody.SourceContent || this is HttpBody.ChannelContent) && + contentLength != null && + (isOneShot || contentLength!! > 65536 * 16) diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 8c42034110..6d39c3b417 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -104,6 +104,14 @@ public final class aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtil public final class aws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { public static final field ACCOUNT_ID_BASED_ENDPOINT Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_CRC32 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_CRC32C Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_SHA1 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_SHA256 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field GZIP_REQUEST_COMPRESSION Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field PAGINATOR Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field PROTOCOL_RPC_V2_CBOR Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; @@ -699,7 +707,12 @@ public final class aws/smithy/kotlin/runtime/hashing/HashFunction$DefaultImpls { public final class aws/smithy/kotlin/runtime/hashing/HashFunctionKt { public static final fun hash ([BLaws/smithy/kotlin/runtime/hashing/HashFunction;)[B public static final fun hash ([BLkotlin/jvm/functions/Function0;)[B + public static final fun isSupportedForFlexibleChecksums (Laws/smithy/kotlin/runtime/hashing/HashFunction;)Z + public static final fun resolveChecksumAlgorithmHeaderName (Laws/smithy/kotlin/runtime/hashing/HashFunction;)Ljava/lang/String; + public static final fun resolveChecksumAlgorithmHeaderName (Ljava/lang/String;)Ljava/lang/String; + public static final fun toBusinessMetric (Laws/smithy/kotlin/runtime/hashing/HashFunction;)Laws/smithy/kotlin/runtime/businessmetrics/BusinessMetric; public static final fun toHashFunction (Ljava/lang/String;)Laws/smithy/kotlin/runtime/hashing/HashFunction; + public static final fun toHashFunctionOrThrow (Ljava/lang/String;)Laws/smithy/kotlin/runtime/hashing/HashFunction; } public final class aws/smithy/kotlin/runtime/hashing/HmacKt { @@ -958,6 +971,8 @@ public final class aws/smithy/kotlin/runtime/io/SdkByteReadChannelKt { public static final fun readFully (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/io/SdkBuffer;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun readRemaining (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/io/SdkBuffer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun readToBuffer (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun rollingHash (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/hashing/HashFunction;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun rollingHash$default (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/hashing/HashFunction;JLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun writeAll (Laws/smithy/kotlin/runtime/io/SdkByteWriteChannel;Laws/smithy/kotlin/runtime/io/SdkSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt index fa9c41652a..0600752a6e 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt @@ -90,6 +90,14 @@ public enum class SmithyBusinessMetric(public override val identifier: String) : SERVICE_ENDPOINT_OVERRIDE("N"), ACCOUNT_ID_BASED_ENDPOINT("O"), SIGV4A_SIGNING("S"), + FLEXIBLE_CHECKSUMS_REQ_CRC32("U"), + FLEXIBLE_CHECKSUMS_REQ_CRC32C("V"), + FLEXIBLE_CHECKSUMS_REQ_SHA1("X"), + FLEXIBLE_CHECKSUMS_REQ_SHA256("Y"), + FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED("Z"), + FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED("a"), + FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED("b"), + FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED("c"), ; override fun toString(): String = identifier diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt index 50684d61d8..2ad5b6785f 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt @@ -4,7 +4,10 @@ */ package aws.smithy.kotlin.runtime.hashing +import aws.smithy.kotlin.runtime.ClientException import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric /** * A cryptographic hash function (algorithm) @@ -70,3 +73,55 @@ public fun String.toHashFunction(): HashFunction? = when (this.lowercase()) { "md5" -> Md5() else -> null } + +/** + * @return The [HashFunction] which is represented by this string, or an exception if none match. + */ +@InternalApi +public fun String.toHashFunctionOrThrow(): HashFunction = + toHashFunction() ?: throw ClientException("Checksum algorithm is not supported: $this") + +/** + * @return If the [HashFunction] is supported by flexible checksums + */ +@InternalApi +public val HashFunction.isSupportedForFlexibleChecksums: Boolean + get() = when (this) { + is Crc32, is Crc32c, is Sha1, is Sha256 -> true + else -> false + } + +/** + * @return The checksum algorithm header used depending on the checksum algorithm name + */ +@InternalApi +public fun String.resolveChecksumAlgorithmHeaderName(): String = + this.toHashFunctionOrThrow().resolveChecksumAlgorithmHeaderName() + +/** + * @return The checksum algorithm header used depending on the checksum algorithm + */ +@InternalApi +public fun HashFunction.resolveChecksumAlgorithmHeaderName(): String { + val prefix = "x-amz-checksum-" + return when (this) { + is Crc32 -> prefix + "crc32" + is Crc32c -> prefix + "crc32c" + is Sha1 -> prefix + "sha1" + is Sha256 -> prefix + "sha256" + is Md5 -> "Content-MD5" + else -> throw ClientException("Checksum algorithm is not supported: ${this::class.simpleName}") + } +} + +/** + * Maps supported hash functions to business metrics. + */ +@InternalApi +public fun HashFunction.toBusinessMetric(): BusinessMetric = when (this) { + is Crc32 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_CRC32 + is Crc32c -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_CRC32C + is Sha1 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_SHA1 + is Sha256 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_SHA256 + else -> throw IllegalStateException("Checksum was calculated using an unsupported hash function: ${this::class.simpleName}") +} diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt index 87d5a531da..a50259e240 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt @@ -4,6 +4,8 @@ */ package aws.smithy.kotlin.runtime.io +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.hashing.HashFunction import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers import kotlinx.coroutines.withContext @@ -131,3 +133,17 @@ public suspend fun SdkByteWriteChannel.writeAll(source: SdkSource): Long = withC } totalRead } + +/** + * Compute the rolling hash of an [SdkByteReadChannel] using [hashFunction], reading up-to [bufferSize] bytes into memory + * @return a ByteArray of the hash function's digest + */ +@InternalApi +public suspend fun SdkByteReadChannel.rollingHash(hashFunction: HashFunction, bufferSize: Long = 8192): ByteArray { + val buffer = SdkBuffer() + while (!isClosedForRead) { + read(buffer, bufferSize) + hashFunction.update(buffer.readToByteArray()) + } + return hashFunction.digest() +} diff --git a/runtime/smithy-client/api/smithy-client.api b/runtime/smithy-client/api/smithy-client.api index c2a9190009..b6132b26b8 100644 --- a/runtime/smithy-client/api/smithy-client.api +++ b/runtime/smithy-client/api/smithy-client.api @@ -235,6 +235,18 @@ public final class aws/smithy/kotlin/runtime/client/config/CompressionClientConf public abstract interface annotation class aws/smithy/kotlin/runtime/client/config/CompressionClientConfigDsl : java/lang/annotation/Annotation { } +public abstract interface class aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig { + public abstract fun getRequestChecksumCalculation ()Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public abstract fun getResponseChecksumValidation ()Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; +} + +public abstract interface class aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig$Builder { + public abstract fun getRequestChecksumCalculation ()Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public abstract fun getResponseChecksumValidation ()Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public abstract fun setRequestChecksumCalculation (Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig;)V + public abstract fun setResponseChecksumValidation (Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig;)V +} + public final class aws/smithy/kotlin/runtime/client/config/RequestCompressionConfig { public static final field Companion Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig$Companion; public fun (Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig$Builder;)V @@ -258,6 +270,22 @@ public final class aws/smithy/kotlin/runtime/client/config/RequestCompressionCon public final fun invoke (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig; } +public final class aws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig : java/lang/Enum { + public static final field WHEN_REQUIRED Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public static final field WHEN_SUPPORTED Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public static fun values ()[Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; +} + +public final class aws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig : java/lang/Enum { + public static final field WHEN_REQUIRED Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public static final field WHEN_SUPPORTED Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public static fun values ()[Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; +} + public final class aws/smithy/kotlin/runtime/client/config/RetryMode : java/lang/Enum { public static final field ADAPTIVE Laws/smithy/kotlin/runtime/client/config/RetryMode; public static final field LEGACY Laws/smithy/kotlin/runtime/client/config/RetryMode; diff --git a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt new file mode 100644 index 0000000000..fbe73b860e --- /dev/null +++ b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.client.config + +/** + * Client config for HTTP checksums + */ +public interface HttpChecksumConfig { + /** + * Configures request checksum calculation + */ + public val requestChecksumCalculation: RequestHttpChecksumConfig? + + /** + * Configures response checksum validation + */ + public val responseChecksumValidation: ResponseHttpChecksumConfig? + + public interface Builder { + /** + * Configures request checksum calculation + */ + public var requestChecksumCalculation: RequestHttpChecksumConfig? + + /** + * Configures response checksum validation + */ + public var responseChecksumValidation: ResponseHttpChecksumConfig? + } +} + +/** + * Configuration options for enabling and managing HTTP request checksums + */ +public enum class RequestHttpChecksumConfig { + /** + * SDK will calculate checksums if the service marks them as required or if the service offers optional checksums. + */ + WHEN_SUPPORTED, + + /** + * SDK will only calculate checksums if the service marks them as required. + */ + WHEN_REQUIRED, +} + +/** + * Configuration options for enabling and managing HTTP response checksums + */ +public enum class ResponseHttpChecksumConfig { + /** + * SDK will validate checksums if the service marks them as required or if the service offers optional checksums. + */ + WHEN_SUPPORTED, + + /** + * SDK will only validate checksums if the service marks them as required. + */ + WHEN_REQUIRED, +} From 14e0958a95a86016d1845c89019a06bc9a0f879c Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 15 Jan 2025 17:58:12 +0000 Subject: [PATCH 24/58] chore: release 1.4.0 --- .changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json | 9 --------- .changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json | 6 ------ .changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json | 8 -------- CHANGELOG.md | 11 +++++++++++ gradle.properties | 4 ++-- 5 files changed, 13 insertions(+), 25 deletions(-) delete mode 100644 .changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json delete mode 100644 .changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json delete mode 100644 .changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json diff --git a/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json b/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json deleted file mode 100644 index d89da89103..0000000000 --- a/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": "0857b6f0-0444-479f-be6b-06ef71d482a0", - "type": "feature", - "description": "⚠️ **IMPORTANT**: Add `retryStrategy` configuration option for waiters", - "issues": [ - "https://github.com/awslabs/aws-sdk-kotlin/issues/1431" - ], - "requiresMinorVersionBump": true -} \ No newline at end of file diff --git a/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json b/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json deleted file mode 100644 index 10ef502204..0000000000 --- a/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "1a68d0b7-00e7-45c0-88f6-95e5c39c9c61", - "type": "misc", - "description": "⚠️ **IMPORTANT**: Upgrade to Kotlin 2.1.0", - "requiresMinorVersionBump": true -} \ No newline at end of file diff --git a/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json b/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json deleted file mode 100644 index 767e48a02e..0000000000 --- a/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "3456d00f-1b29-4d88-ab02-045db1b1ebce", - "type": "bugfix", - "description": "Include more information when retry strategy halts early due to token bucket capacity errors", - "issues": [ - "awslabs/aws-sdk-kotlin#1321" - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c1758b4332..c324c5e681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.4.0] - 01/15/2025 + +### Features +* [#1431](https://github.com/awslabs/aws-sdk-kotlin/issues/1431) ⚠️ **IMPORTANT**: Add `retryStrategy` configuration option for waiters + +### Fixes +* [#1321](https://github.com/awslabs/aws-sdk-kotlin/issues/1321) Include more information when retry strategy halts early due to token bucket capacity errors + +### Miscellaneous +* ⚠️ **IMPORTANT**: Upgrade to Kotlin 2.1.0 + ## [1.3.34] - 01/10/2025 ## [1.3.33] - 01/10/2025 diff --git a/gradle.properties b/gradle.properties index d76819491b..d0e2da8b41 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.35-SNAPSHOT +sdkVersion=1.4.0 # codegen -codegenVersion=0.33.35-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.0 \ No newline at end of file From ed95d7b5014ba9311d4a3fff16671971fc026e36 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 15 Jan 2025 17:58:13 +0000 Subject: [PATCH 25/58] chore: bump snapshot version to 1.4.1-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index d0e2da8b41..dbe31b2197 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.0 +sdkVersion=1.4.1-SNAPSHOT # codegen -codegenVersion=0.34.0 \ No newline at end of file +codegenVersion=0.34.1-SNAPSHOT \ No newline at end of file From a4ace353ca9dc1517c00de345a833691c444841d Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 16 Jan 2025 11:50:34 -0500 Subject: [PATCH 26/58] fix: add 0.9.x aws-crt-kotlin transform (#1220) --- .brazil.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.brazil.json b/.brazil.json index 5a1d830abd..dbb2135d1b 100644 --- a/.brazil.json +++ b/.brazil.json @@ -8,6 +8,7 @@ "com.squareup.okio:okio-jvm:3.*": "OkioJvm-3.x", "io.opentelemetry:opentelemetry-api:1.*": "Maven-io-opentelemetry_opentelemetry-api-1.x", "org.slf4j:slf4j-api:2.*": "Maven-org-slf4j_slf4j-api-2.x", + "aws.sdk.kotlin.crt:aws-crt-kotlin:0.9.*": "AwsCrtKotlin-0.9.x", "aws.sdk.kotlin.crt:aws-crt-kotlin:0.8.*": "AwsCrtKotlin-0.8.x", "com.squareup.okhttp3:okhttp:4.*": "OkHttp3-4.x" }, From 15e5f0ce2c713f3e81f75a28e90b4175891e0575 Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 16 Jan 2025 14:57:24 -0500 Subject: [PATCH 27/58] fix: Ensure `Host` header is included when signing auth tokens (#1222) --- .github/workflows/continuous-integration.yml | 2 +- gradle/libs.versions.toml | 2 +- .../kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt | 9 ++++++++- .../runtime/auth/awssigning/AuthTokenGeneratorTest.kt | 2 ++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 599293cc06..ef2cc9db42 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -68,7 +68,7 @@ jobs: ./gradlew test jvmTest - name: Save Test Reports if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-reports-${{ matrix.os }} path: '**/build/reports' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6ef5b35d57..6d8724016e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin-version = "2.1.0" dokka-version = "1.9.10" -aws-kotlin-repo-tools-version = "0.4.17" +aws-kotlin-repo-tools-version = "0.4.18" # libs coroutines-version = "1.9.0" diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt index e92ac1f72c..f80ee4fe7e 100644 --- a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.auth.awssigning import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke +import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.HttpMethod import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.net.url.Url @@ -28,7 +29,13 @@ public class AuthTokenGenerator( private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://") public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { - val req = HttpRequest(HttpMethod.GET, endpoint) + val req = HttpRequest( + HttpMethod.GET, + endpoint, + headers = Headers { + append("Host", endpoint.hostAndPort) + }, + ) val config = AwsSigningConfig { credentials = credentialsProvider.resolve() diff --git a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt index 87fec1a74e..75b17fbab9 100644 --- a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt +++ b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt @@ -45,6 +45,7 @@ class AuthTokenGeneratorTest { assertContains(token, "X-Amz-Credential=signature") // test custom signer was invoked assertContains(token, "X-Amz-Expires=333") // expiration assertContains(token, "X-Amz-SigningDate=0") // clock + assertContains(token, "X-Amz-SignedHeaders=host") assertTrue(credentialsProvider.credentialsResolved) } @@ -60,6 +61,7 @@ private val TEST_SIGNER = object : AwsSigner { put("X-Amz-Credential", "signature") put("X-Amz-Expires", (config.expiresAfter?.toLong(DurationUnit.SECONDS) ?: 900).toString()) put("X-Amz-SigningDate", config.signingDate.epochSeconds.toString()) + put("X-Amz-SignedHeaders", request.headers.names().map { it.lowercase() }.joinToString()) } return AwsSigningResult(builder.build(), "signature".encodeToByteArray()) From 447ac10880ebe778471d9ad4bce97a424139550d Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 16 Jan 2025 19:59:39 +0000 Subject: [PATCH 28/58] chore: release 1.4.1 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c324c5e681..a4c4a65548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.4.1] - 01/16/2025 + ## [1.4.0] - 01/15/2025 ### Features diff --git a/gradle.properties b/gradle.properties index dbe31b2197..e005c08ee2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.1-SNAPSHOT +sdkVersion=1.4.1 # codegen -codegenVersion=0.34.1-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.1 \ No newline at end of file From 0f8db4452966822949bbe6616e3642b2063cfb63 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 16 Jan 2025 19:59:40 +0000 Subject: [PATCH 29/58] chore: bump snapshot version to 1.4.2-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index e005c08ee2..df0e651ce1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.1 +sdkVersion=1.4.2-SNAPSHOT # codegen -codegenVersion=0.34.1 \ No newline at end of file +codegenVersion=0.34.2-SNAPSHOT \ No newline at end of file From 8b336937bb028bc3d0fcb1beafeabcf4c25fc51e Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 23 Jan 2025 14:03:54 -0500 Subject: [PATCH 30/58] fix: address various failing protocol tests (#1223) --- .../model/error-correction-tests.smithy | 12 ++++---- .../core/AwsHttpBindingProtocolGenerator.kt | 8 +---- .../codegen/core/KotlinSymbolProvider.kt | 30 ++++++++++++------- .../codegen/rendering/StructureGenerator.kt | 1 + .../protocol/HttpStringValuesMapSerializer.kt | 19 ++---------- .../kotlin/codegen/core/SymbolProviderTest.kt | 2 +- .../HttpBindingProtocolGeneratorTest.kt | 8 ++--- .../HttpStringValuesMapSerializerTest.kt | 16 +++++----- gradle/libs.versions.toml | 2 +- .../kotlin/runtime/text/encoding/Base64.kt | 11 +++++-- .../runtime/text/encoding/Base64Test.kt | 28 ++++++++++++----- 11 files changed, 74 insertions(+), 63 deletions(-) diff --git a/codegen/protocol-tests/model/error-correction-tests.smithy b/codegen/protocol-tests/model/error-correction-tests.smithy index 201ffc877f..4f246e1b99 100644 --- a/codegen/protocol-tests/model/error-correction-tests.smithy +++ b/codegen/protocol-tests/model/error-correction-tests.smithy @@ -39,8 +39,9 @@ operation SayHelloXml { output: TestOutput, errors: [Error] } structure TestOutputDocument with [TestStruct] { innerField: Nested, - // FIXME: This trait fails smithy validator - // @required + + // Note: This shape _should_ be @required, but causes Smithy httpResponseTests validation to fail. + // We expect `document` to be deserialized as `null` and enforce @required using a runtime check, but Smithy validator doesn't recognize / allow this. document: Document } structure TestOutput with [TestStruct] { innerField: Nested } @@ -65,8 +66,8 @@ structure TestStruct { @required nestedListValue: NestedList - // FIXME: This trait fails smithy validator - // @required + // Note: This shape _should_ be @required, but causes Smithy httpResponseTests validation to fail. + // We expect `nested` to be deserialized as `null` and enforce @required using a runtime check, but Smithy validator doesn't recognize / allow this. nested: Nested @required @@ -97,8 +98,7 @@ union MyUnion { } structure Nested { - // FIXME: This trait fails smithy validator - // @required + @required a: String } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt index 6e5834f7d1..bd41afcade 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt @@ -40,13 +40,7 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator() // val targetedTest = TestMemberDelta(setOf("RestJsonComplexErrorWithNoMessage"), TestContainmentMode.RUN_TESTS) val ignoredTests = TestMemberDelta( - setOf( - "AwsJson10ClientErrorCorrectsWithDefaultValuesWhenServerFailsToSerializeRequiredValues", - "RestJsonNullAndEmptyHeaders", - "NullAndEmptyHeaders", - "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", - "RpcV2CborClientPopulatesDefaultValuesInInput", - ), + setOf(), ) val requestTestBuilder = HttpProtocolUnitTestRequestGenerator.Builder() diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt index e6dbd0dca1..f35570dfeb 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt @@ -193,7 +193,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli } else { // only use @default if type is `T` shape.getTrait()?.let { - defaultValue(it.getDefaultValue(targetShape)) + setDefaultValue(it, targetShape) } } } @@ -219,9 +219,10 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli } } - private fun DefaultTrait.getDefaultValue(targetShape: Shape): String? { - val node = toNode() - return when { + private fun Symbol.Builder.setDefaultValue(defaultTrait: DefaultTrait, targetShape: Shape) { + val node = defaultTrait.toNode() + + val defaultValue = when { node.toString() == "null" -> null // Check if target is an enum before treating the default like a regular number/string @@ -235,13 +236,20 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli "${enumSymbol.fullName}.fromValue($arg)" } - targetShape.isBlobShape && targetShape.isStreaming -> - node - .toString() - .takeUnless { it.isEmpty() } - ?.let { "ByteStream.fromString(${it.dq()})" } + targetShape.isBlobShape -> { + addReferences(RuntimeTypes.Core.Text.Encoding.decodeBase64) - targetShape.isBlobShape -> "${node.toString().dq()}.encodeToByteArray()" + if (targetShape.isStreaming) { + node.toString() + .takeUnless { it.isEmpty() } + ?.let { + addReferences(RuntimeTypes.Core.Content.ByteStream) + "ByteStream.fromString(${it.dq()}.decodeBase64())" + } + } else { + "${node.toString().dq()}.decodeBase64().encodeToByteArray()" + } + } targetShape.isDocumentShape -> getDefaultValueForDocument(node) targetShape.isTimestampShape -> getDefaultValueForTimestamp(node.asNumberNode().get()) @@ -252,6 +260,8 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli node.isStringNode -> node.toString().dq() else -> node.toString() } + + defaultValue(defaultValue) } private fun getDefaultValueForTimestamp(node: NumberNode): String { diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt index c34eb5ab9d..5d90f376d2 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt @@ -248,6 +248,7 @@ class StructureGenerator( } else { memberSymbol } + write("public var #L: #E", memberName, builderMemberSymbol) } write("") diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt index 932b2a796a..5dfc519962 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt @@ -157,9 +157,8 @@ class HttpStringValuesMapSerializer( val paramName = binding.locationName // addAll collection parameter 2 val param2 = if (mapFnContents.isEmpty()) "input.$memberName" else "input.$memberName.map { $mapFnContents }" - val nullCheck = if (memberSymbol.isNullable) "?" else "" writer.write( - "if (input.#L$nullCheck.isNotEmpty() == true) #L(#S, #L)", + "if (input.#L != null) #L(#S, #L)", memberName, binding.location.addAllFnName, paramName, @@ -174,8 +173,7 @@ class HttpStringValuesMapSerializer( val paramName = binding.locationName val memberSymbol = symbolProvider.toSymbol(binding.member) - // NOTE: query parameters are allowed to be empty, whereas headers should omit empty string - // values from serde + // NOTE: query parameters are allowed to be empty if ((location == HttpBinding.Location.QUERY || location == HttpBinding.Location.HEADER) && binding.member.hasTrait()) { // Call the idempotency token function if no supplied value. writer.addImport(RuntimeTypes.SmithyClient.IdempotencyTokenProviderExt) @@ -185,18 +183,7 @@ class HttpStringValuesMapSerializer( paramName, ) } else { - val nullCheck = - if (location == HttpBinding.Location.QUERY || - memberTarget.hasTrait< - @Suppress("DEPRECATION") - software.amazon.smithy.model.traits.EnumTrait, - >() - ) { - if (memberSymbol.isNullable) "input.$memberName != null" else "" - } else { - val nullCheck = if (memberSymbol.isNullable) "?" else "" - "input.$memberName$nullCheck.isNotEmpty() == true" - } + val nullCheck = if (memberSymbol.isNullable) "input.$memberName != null" else "" val cond = defaultCheck(binding.member) ?: nullCheck diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt index 0cc4d2cb14..bb90fbc0fb 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt @@ -182,7 +182,7 @@ class SymbolProviderTest { "double,2.71828,2.71828", "byte,10,10.toByte()", "string,\"hello\",\"hello\"", - "blob,\"abcdefg\",\"abcdefg\".encodeToByteArray()", + "blob,\"abcdefg\",\"abcdefg\".decodeBase64().encodeToByteArray()", "boolean,true,true", "bigInteger,5,5", "bigDecimal,9.0123456789,9.0123456789", diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt index 3c15579dab..6f8040e7dc 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt @@ -57,8 +57,8 @@ internal class SmokeTestOperationSerializer: HttpSerializer.NonStreaming( + "YQ" to "a", + "Yg" to "b", + "YWI" to "ab", + "YWJj" to "abc", + "SGVsbG8gd29ybGQ" to "Hello world", + ) + + inputs.forEach { (encoded, expected) -> + val actual = encoded.decodeBase64() + assertEquals(expected, actual) + } + } } From 03badf913813df5481b330e9eeff80bcea5586ec Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 23 Jan 2025 14:04:09 -0500 Subject: [PATCH 31/58] misc: re-enable `kotlinWarningsAsErrors=true` (#1224) --- .github/workflows/continuous-integration.yml | 3 +-- .../kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt | 2 +- .../smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt | 2 ++ .../smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt | 2 +- .../smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt | 2 +- .../common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt | 3 +-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ef2cc9db42..d47020ea5d 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -62,8 +62,7 @@ jobs: - name: Test shell: bash run: | - # FIXME K2. Re-enable warnings as errors after this warning is removed: https://youtrack.jetbrains.com/issue/KT-68532 - # echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties + echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties ./gradlew apiCheck ./gradlew test jvmTest - name: Save Test Reports diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt index 0ea8ae5e66..8047b870c3 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt @@ -30,7 +30,7 @@ public object MetricsInterceptor : Interceptor { } val originalResponse = chain.proceed(request) - val response = if (originalResponse.body == null || originalResponse.body?.contentLength() == 0L) { + val response = if (originalResponse.body.contentLength() == 0L) { originalResponse } else { originalResponse.newBuilder() diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt index 061c22a13d..a20387d0a0 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt @@ -15,6 +15,7 @@ import aws.smithy.kotlin.runtime.net.TlsVersion import aws.smithy.kotlin.runtime.operation.ExecutionContext import aws.smithy.kotlin.runtime.time.Instant import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.job import okhttp3.* import okhttp3.ConnectionPool @@ -47,6 +48,7 @@ public class OkHttpEngine( private val connectionIdleMonitor = config.connectionIdlePollingInterval?.let { ConnectionIdleMonitor(it) } private val client = config.buildClientWithConnectionListener(metrics, connectionIdleMonitor) + @OptIn(ExperimentalCoroutinesApi::class) override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { val callContext = callContext() diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt index c287228fdf..181ed400c3 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt @@ -104,7 +104,7 @@ public fun Headers.toOkHttpHeaders(): OkHttpHeaders = OkHttpHeaders.Builder().al @InternalApi public fun OkHttpResponse.toSdkResponse(): HttpResponse { val sdkHeaders = OkHttpHeadersAdapter(headers) - val httpBody = if (body == null || body!!.contentLength() == 0L) { + val httpBody = if (body.contentLength() == 0L) { HttpBody.Empty } else { object : HttpBody.SourceContent() { diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt index ff59823201..026bbd942c 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt @@ -99,7 +99,7 @@ private suspend fun Call.executeAsync(): Response = call: Call, response: Response, ) { - continuation.resume(response) { + continuation.resume(response) { cause, _, _ -> response.closeQuietly() } } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt index 9b691a79a2..c8437b2b33 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt @@ -62,8 +62,7 @@ public fun Any?.type(): String = when (this) { is List<*>, is Array<*> -> "array" is Number -> "number" is Any -> "object" - null -> "null" - else -> throw Exception("Undetected type for: $this") + else -> "null" } // Collection `flattenIfPossible` functions From acae410058a26cb0d4565f915c658108b3154fe9 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 28 Jan 2025 10:49:21 -0500 Subject: [PATCH 32/58] Fix `decompressGzipBytes` duplication --- runtime/protocol/http-client/build.gradle.kts | 1 + .../RequestCompressionInterceptorTest.kt | 11 +---------- .../RequestCompressionInterceptorTestJvm.kt | 10 ---------- .../RequestCompressionInterceptorTestNative.kt | 9 --------- .../kotlin/runtime/compression}/GzipTestUtils.kt | 7 +++++-- .../kotlin/runtime/compression/GzipTest.kt | 1 - .../kotlin/runtime/io/GzipByteReadChannelTest.kt | 1 + .../kotlin/runtime/io/GzipSdkSourceTest.kt | 1 + .../runtime/compression/GzipTestUtilsJvm.kt | 16 ++++++++++++++++ .../smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt | 13 ------------- .../runtime/compression}/GzipTestUtilsNative.kt | 6 ++++-- 11 files changed, 29 insertions(+), 47 deletions(-) delete mode 100644 runtime/protocol/http-client/jvm/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTestJvm.kt delete mode 100644 runtime/protocol/http-client/native/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTestNative.kt rename runtime/runtime-core/common/{test/aws/smithy/kotlin/runtime/io => src/aws/smithy/kotlin/runtime/compression}/GzipTestUtils.kt (52%) create mode 100644 runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsJvm.kt rename runtime/runtime-core/native/{test/aws/smithy/kotlin/runtime/io => src/aws/smithy/kotlin/runtime/compression}/GzipTestUtilsNative.kt (93%) diff --git a/runtime/protocol/http-client/build.gradle.kts b/runtime/protocol/http-client/build.gradle.kts index d55e5ffe41..b485f29393 100644 --- a/runtime/protocol/http-client/build.gradle.kts +++ b/runtime/protocol/http-client/build.gradle.kts @@ -27,6 +27,7 @@ kotlin { commonTest { dependencies { implementation(libs.kotlinx.coroutines.test) + implementation(project(":runtime:runtime-core")) implementation(project(":runtime:protocol:http-test")) } } diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt index dc847a8839..f26e67d0a2 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt @@ -5,7 +5,7 @@ package aws.smithy.kotlin.runtime.http.interceptors -import aws.smithy.kotlin.runtime.IgnoreNative +import aws.smithy.kotlin.runtime.compression.decompressGzipBytes import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.compression.CompressionAlgorithm import aws.smithy.kotlin.runtime.compression.Gzip @@ -23,8 +23,6 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith -internal expect fun decompressGzipBytes(bytes: ByteArray): ByteArray - class RequestCompressionInterceptorTest { private val client = SdkHttpClient(TestEngine()) @@ -60,7 +58,6 @@ class RequestCompressionInterceptorTest { return op.context.attributes[HttpOperationContext.HttpCallList].first() } - @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testCompressionThresholdTooHigh() = runTest { val payload = "bar" @@ -80,7 +77,6 @@ class RequestCompressionInterceptorTest { assertEquals(bytes, sentBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testCompression() = runTest { val payload = "bar" @@ -101,7 +97,6 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testSdkSource() = runTest { val payload = "bar" @@ -122,7 +117,6 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testSdkByteReadChannel() = runTest { val payload = "bar" @@ -143,7 +137,6 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testHeaderAlreadySet() = runTest { val payload = "bar" @@ -165,7 +158,6 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testNoSupportedAlgorithms() = runTest { val payload = "bar" @@ -185,7 +177,6 @@ class RequestCompressionInterceptorTest { assertEquals(bytes, sentBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testInvalidCompressionThreshold() = runTest { val payload = "bar" diff --git a/runtime/protocol/http-client/jvm/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTestJvm.kt b/runtime/protocol/http-client/jvm/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTestJvm.kt deleted file mode 100644 index a30c707658..0000000000 --- a/runtime/protocol/http-client/jvm/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTestJvm.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.smithy.kotlin.runtime.http.interceptors - -import java.util.zip.GZIPInputStream - -internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray = - GZIPInputStream(bytes.inputStream()).use { it.readBytes() } diff --git a/runtime/protocol/http-client/native/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTestNative.kt b/runtime/protocol/http-client/native/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTestNative.kt deleted file mode 100644 index 7d55f18341..0000000000 --- a/runtime/protocol/http-client/native/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTestNative.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.smithy.kotlin.runtime.http.interceptors - -internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { - TODO("Not yet implemented. Can we write a pure Kotlin implementation?") -} diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipTestUtils.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/compression/GzipTestUtils.kt similarity index 52% rename from runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipTestUtils.kt rename to runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/compression/GzipTestUtils.kt index 511fe59f43..31b014acd0 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipTestUtils.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/compression/GzipTestUtils.kt @@ -2,9 +2,12 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -package aws.smithy.kotlin.runtime.io +package aws.smithy.kotlin.runtime.compression + +import aws.smithy.kotlin.runtime.InternalApi /** * Decompresses a [ByteArray] compressed using the gzip format */ -internal expect fun decompressGzipBytes(bytes: ByteArray): ByteArray +@InternalApi +public expect fun decompressGzipBytes(bytes: ByteArray): ByteArray diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt index de1199a6bc..0125eae489 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt @@ -7,7 +7,6 @@ package aws.smithy.kotlin.runtime.compression import aws.smithy.kotlin.runtime.content.ByteStream import aws.smithy.kotlin.runtime.content.toByteArray -import aws.smithy.kotlin.runtime.io.decompressGzipBytes import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertContentEquals diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipByteReadChannelTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipByteReadChannelTest.kt index ad1c9bf5b6..06672e4f5c 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipByteReadChannelTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipByteReadChannelTest.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.IgnoreNative +import aws.smithy.kotlin.runtime.compression.decompressGzipBytes import aws.smithy.kotlin.runtime.hashing.crc32 import kotlinx.coroutines.test.runTest import kotlin.test.Test diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipSdkSourceTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipSdkSourceTest.kt index f36b671894..8fbe370ce7 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipSdkSourceTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipSdkSourceTest.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.IgnoreNative +import aws.smithy.kotlin.runtime.compression.decompressGzipBytes import aws.smithy.kotlin.runtime.hashing.crc32 import kotlinx.coroutines.test.runTest import kotlin.test.Test diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsJvm.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsJvm.kt new file mode 100644 index 0000000000..bd2b274962 --- /dev/null +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsJvm.kt @@ -0,0 +1,16 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.compression + +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.io.use +import java.util.zip.GZIPInputStream + +/** + * Decompresses a [ByteArray] compressed using the gzip format + */ +@InternalApi +public actual fun decompressGzipBytes(bytes: ByteArray): ByteArray = + GZIPInputStream(bytes.inputStream()).use { it.readBytes() } diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt index 1d879aa925..e69de29bb2 100644 --- a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt +++ b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt @@ -1,13 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.smithy.kotlin.runtime.io - -import java.util.zip.GZIPInputStream - -/** - * Decompresses a [ByteArray] compressed using the gzip format - */ -internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray = - GZIPInputStream(bytes.inputStream()).use { it.readBytes() } diff --git a/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt similarity index 93% rename from runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt rename to runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt index f75d5e0055..173515d455 100644 --- a/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt @@ -2,8 +2,9 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ -package aws.smithy.kotlin.runtime.io +package aws.smithy.kotlin.runtime.compression +import aws.smithy.kotlin.runtime.InternalApi import kotlinx.cinterop.addressOf import kotlinx.cinterop.alloc import kotlinx.cinterop.memScoped @@ -17,7 +18,8 @@ import platform.zlib.* /** * Decompresses a byte array compressed using the gzip format */ -internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { +@InternalApi +public actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { if (bytes.isEmpty()) { return bytes } From 95350cb60b4c74638b111cbc58cb99a6ac04e481 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 28 Jan 2025 10:50:50 -0500 Subject: [PATCH 33/58] Replace `@IgnoreNative`, still missing some implementations --- .../interceptors/RequestCompressionInterceptorTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt index f26e67d0a2..6925b909a6 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.http.interceptors +import aws.smithy.kotlin.runtime.IgnoreNative import aws.smithy.kotlin.runtime.compression.decompressGzipBytes import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.compression.CompressionAlgorithm @@ -58,6 +59,7 @@ class RequestCompressionInterceptorTest { return op.context.attributes[HttpOperationContext.HttpCallList].first() } + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testCompressionThresholdTooHigh() = runTest { val payload = "bar" @@ -77,6 +79,7 @@ class RequestCompressionInterceptorTest { assertEquals(bytes, sentBytes) } + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testCompression() = runTest { val payload = "bar" @@ -97,6 +100,7 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testSdkSource() = runTest { val payload = "bar" @@ -117,6 +121,7 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testSdkByteReadChannel() = runTest { val payload = "bar" @@ -137,6 +142,7 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testHeaderAlreadySet() = runTest { val payload = "bar" @@ -158,6 +164,7 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testNoSupportedAlgorithms() = runTest { val payload = "bar" @@ -177,6 +184,7 @@ class RequestCompressionInterceptorTest { assertEquals(bytes, sentBytes) } + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testInvalidCompressionThreshold() = runTest { val payload = "bar" From 913f5341c28536d451a5cdc7ab6b83c6b54f9ba8 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 28 Jan 2025 12:38:55 -0500 Subject: [PATCH 34/58] Fix duplicated `decompressGzipBytes` --- runtime/runtime-core/api/runtime-core.api | 4 ++++ .../jvm/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt | 0 2 files changed, 4 insertions(+) delete mode 100644 runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 835afc0e39..e46e62bd0c 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -330,6 +330,10 @@ public final class aws/smithy/kotlin/runtime/compression/Gzip : aws/smithy/kotli public fun getId ()Ljava/lang/String; } +public final class aws/smithy/kotlin/runtime/compression/GzipTestUtilsJvmKt { + public static final fun decompressGzipBytes ([B)[B +} + public final class aws/smithy/kotlin/runtime/config/EnvironmentSetting { public static final field Companion Laws/smithy/kotlin/runtime/config/EnvironmentSetting$Companion; public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt deleted file mode 100644 index e69de29bb2..0000000000 From 9f44cdbd82adc1dd30eaf4bc4290b61eee09bbd4 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:33:23 -0800 Subject: [PATCH 35/58] fix: ignore hop-by-hop headers when signing requests (#1227) --- .../b0646077-397d-48a8-94f2-ea2db40b1dea.json | 5 +++++ gradle/libs.versions.toml | 2 +- .../runtime/auth/awssigning/Canonicalizer.kt | 17 ++++++++++++++--- .../auth/awssigning/DefaultCanonicalizerTest.kt | 9 +++++++-- .../runtime/http/auth/AwsHttpSignerTestBase.kt | 16 +++++++++------- 5 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 .changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json diff --git a/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json b/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json new file mode 100644 index 0000000000..96e13a4a82 --- /dev/null +++ b/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json @@ -0,0 +1,5 @@ +{ + "id": "b0646077-397d-48a8-94f2-ea2db40b1dea", + "type": "bugfix", + "description": "Ignore hop-by-hop headers when signing requests" +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31f5d17afa..bdc0c8e98b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ okio-version = "3.9.1" otel-version = "1.45.0" slf4j-version = "2.0.16" slf4j-v1x-version = "1.7.36" -crt-kotlin-version = "0.9.0" +crt-kotlin-version = "0.9.1" micrometer-version = "1.14.2" binary-compatibility-validator-version = "0.16.3" diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt index 69d742de79..c7c47fe5a2 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt @@ -58,16 +58,27 @@ internal interface Canonicalizer { ): CanonicalRequest } -// Taken from https://github.com/awslabs/aws-c-auth/blob/dd505b55fd46222834f35c6e54165d8cbebbfaaa/source/aws_signing.c#L118-L156 private val skipHeaders = setOf( - "connection", "expect", // https://github.com/awslabs/aws-sdk-kotlin/issues/862 + + // Taken from https://github.com/awslabs/aws-c-auth/blob/274a1d21330731cc51bb742794adc70ada5f4380/source/aws_signing.c#L121-L164 "sec-websocket-key", "sec-websocket-protocol", "sec-websocket-version", - "upgrade", "user-agent", "x-amzn-trace-id", + + // Taken from https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1. These are "hop-by-hop" headers which may + // be modified/removed by intervening proxies or caches. These are unsafe to sign because if they change they render + // the signature invalid. + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", ) internal class DefaultCanonicalizer(private val sha256Supplier: HashSupplier = ::Sha256) : Canonicalizer { diff --git a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt index 7fc20ddb83..2dfbdf48e1 100644 --- a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt +++ b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt @@ -6,8 +6,12 @@ package aws.smithy.kotlin.runtime.auth.awssigning import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awssigning.tests.DEFAULT_TEST_CREDENTIALS -import aws.smithy.kotlin.runtime.http.* -import aws.smithy.kotlin.runtime.http.request.* +import aws.smithy.kotlin.runtime.http.Headers +import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.headers +import aws.smithy.kotlin.runtime.http.request.url import aws.smithy.kotlin.runtime.net.Host import aws.smithy.kotlin.runtime.net.url.Url import aws.smithy.kotlin.runtime.time.Instant @@ -136,6 +140,7 @@ class DefaultCanonicalizerTest { // These should not be signed set("Expect", "100-continue") set("X-Amzn-Trace-Id", "qux") + set("Transfer-Encoding", "chunked") } body = HttpBody.Empty } diff --git a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt index 2ce308df8f..7d10b35e14 100644 --- a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt +++ b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt @@ -12,7 +12,9 @@ import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner import aws.smithy.kotlin.runtime.auth.awssigning.internal.AWS_CHUNKED_THRESHOLD import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.collections.get -import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.SdkHttpClient import aws.smithy.kotlin.runtime.http.operation.* import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder @@ -149,8 +151,8 @@ public abstract class AwsHttpSignerTestBase( val op = buildOperation(streaming = true, replayable = false, requestBody = "a".repeat(AWS_CHUNKED_THRESHOLD + 1)) val expectedDate = "20201016T195600Z" val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + - "Signature=ac341b9b248a0b23d2fcd9f7e805f4eb0b8a1b789bb23a8ec6adc6c48dd084ad" + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "Signature=ef06c95647c4d2daa6c89ac90274f1c780777cba8eaab772df6d8009def3eb8f" val signed = getSignedRequest(op) assertEquals(expectedDate, signed.headers["X-Amz-Date"]) @@ -162,8 +164,8 @@ public abstract class AwsHttpSignerTestBase( val op = buildOperation(streaming = true, replayable = true, requestBody = "a".repeat(AWS_CHUNKED_THRESHOLD + 1)) val expectedDate = "20201016T195600Z" val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + - "Signature=ac341b9b248a0b23d2fcd9f7e805f4eb0b8a1b789bb23a8ec6adc6c48dd084ad" + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "Signature=ef06c95647c4d2daa6c89ac90274f1c780777cba8eaab772df6d8009def3eb8f" val signed = getSignedRequest(op) assertEquals(expectedDate, signed.headers["X-Amz-Date"]) @@ -176,8 +178,8 @@ public abstract class AwsHttpSignerTestBase( val expectedDate = "20201016T195600Z" // should have same signature as testSignAwsChunkedStreamNonReplayable(), except for the hash, since the body is different val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + - "Signature=3f0277123c9ed8a8858f793886a0ac0fcb457bc54401ffc22d470f373397cff0" + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "Signature=a902702b57057a864bf41cc22ee846a1b7bd047e22784367ec6a459f6791330e" val signed = getSignedRequest(op) assertEquals(expectedDate, signed.headers["X-Amz-Date"]) From 97ac44792df8016d27988bcde1d112c69b46ab29 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 28 Jan 2025 18:35:54 +0000 Subject: [PATCH 36/58] chore: release 1.4.2 --- .changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json diff --git a/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json b/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json deleted file mode 100644 index 96e13a4a82..0000000000 --- a/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "b0646077-397d-48a8-94f2-ea2db40b1dea", - "type": "bugfix", - "description": "Ignore hop-by-hop headers when signing requests" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c4a65548..2e925d0943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.4.2] - 01/28/2025 + +### Fixes +* Ignore hop-by-hop headers when signing requests + ## [1.4.1] - 01/16/2025 ## [1.4.0] - 01/15/2025 diff --git a/gradle.properties b/gradle.properties index df0e651ce1..1b11779bb6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.2-SNAPSHOT +sdkVersion=1.4.2 # codegen -codegenVersion=0.34.2-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.2 \ No newline at end of file From b7e50e3c66a073e5696b363aae29823284beef09 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 28 Jan 2025 18:35:55 +0000 Subject: [PATCH 37/58] chore: bump snapshot version to 1.4.3-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1b11779bb6..9e53dbcaf0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.2 +sdkVersion=1.4.3-SNAPSHOT # codegen -codegenVersion=0.34.2 \ No newline at end of file +codegenVersion=0.34.3-SNAPSHOT \ No newline at end of file From edd846aaf5a8aed8d75f22b1b9038c80f75940dd Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Wed, 29 Jan 2025 15:55:49 -0500 Subject: [PATCH 38/58] Implement `GzipCompressor`, enable all gzip tests --- .../runtime/io/GzipByteReadChannelTest.kt | 12 -- .../kotlin/runtime/io/GzipSdkSourceTest.kt | 6 - .../runtime/compression/GzipCompressor.kt | 130 ++++++++++++++++++ .../kotlin/runtime/compression/GzipNative.kt | 59 +------- .../compression/GzipTestUtilsNative.kt | 43 +++--- .../runtime/io/GzipByteReadChannelNative.kt | 37 +++-- .../kotlin/runtime/io/GzipSdkSourceNative.kt | 29 +++- 7 files changed, 205 insertions(+), 111 deletions(-) create mode 100644 runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipByteReadChannelTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipByteReadChannelTest.kt index 06672e4f5c..259b4a3aab 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipByteReadChannelTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipByteReadChannelTest.kt @@ -4,7 +4,6 @@ */ package aws.smithy.kotlin.runtime.io -import aws.smithy.kotlin.runtime.IgnoreNative import aws.smithy.kotlin.runtime.compression.decompressGzipBytes import aws.smithy.kotlin.runtime.hashing.crc32 import kotlinx.coroutines.test.runTest @@ -13,7 +12,6 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals class GzipByteReadChannelTest { - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadAll() = runTest { val payload = "Hello World" @@ -33,7 +31,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadToBuffer() = runTest { val payload = "Hello World".repeat(1600) @@ -52,7 +49,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadRemaining() = runTest { val payload = "Hello World".repeat(1600) @@ -72,7 +68,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testRead() = runTest { val payload = "Hello World" @@ -93,7 +88,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadLargeBody() = runTest { val payload = "Hello World".repeat(1600) @@ -114,7 +108,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadLargeLimit() = runTest { val payload = "Hello World" @@ -135,7 +128,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadLargeBodyLargeLimit() = runTest { val payload = "Hello World".repeat(1600) @@ -156,7 +148,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testIsClosedForRead() = runTest { val payload = "Hello World" @@ -179,7 +170,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testIsClosedForReadLargeBody() = runTest { val payload = "Hello World".repeat(1600) @@ -202,7 +192,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testIsClosedForReadLargeLimit() = runTest { val payload = "Hello World" @@ -225,7 +214,6 @@ class GzipByteReadChannelTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testIsClosedForReadLargeBodyLargeLimit() = runTest { val payload = "Hello World".repeat(1600) diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipSdkSourceTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipSdkSourceTest.kt index 8fbe370ce7..179673c28a 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipSdkSourceTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipSdkSourceTest.kt @@ -4,7 +4,6 @@ */ package aws.smithy.kotlin.runtime.io -import aws.smithy.kotlin.runtime.IgnoreNative import aws.smithy.kotlin.runtime.compression.decompressGzipBytes import aws.smithy.kotlin.runtime.hashing.crc32 import kotlinx.coroutines.test.runTest @@ -13,7 +12,6 @@ import kotlin.test.assertContentEquals import kotlin.test.assertEquals class GzipSdkSourceTest { - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadToByteArray() = runTest { val payload = "Hello World" @@ -31,7 +29,6 @@ class GzipSdkSourceTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testRead() = runTest { val payload = "Hello World" @@ -52,7 +49,6 @@ class GzipSdkSourceTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadLargeBody() = runTest { val payload = "Hello World".repeat(1600) @@ -74,7 +70,6 @@ class GzipSdkSourceTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadLargeLimit() = runTest { val payload = "Hello World" @@ -95,7 +90,6 @@ class GzipSdkSourceTest { assertEquals(bytesHash, decompressedBytes.crc32()) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testReadLargeBodyLargeLimit() = runTest { val payload = "Hello World".repeat(1600) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt new file mode 100644 index 0000000000..7f5a4b251a --- /dev/null +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -0,0 +1,130 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.compression + +import kotlinx.atomicfu.atomic +import kotlinx.cinterop.* +import platform.zlib.* + +private const val DEFAULT_WINDOW_BITS = 15 +private const val GZIP_ENCODING = 16 // Add this to windowBits for gzip encoding +private const val COMPRESSION_LEVEL = Z_BEST_COMPRESSION + +/** + * Streaming-style gzip compressor, implemented using zlib bindings + */ +internal class GzipCompressor { + companion object { + internal const val BUFFER_SIZE = 16384 + } + + private val buffer = ByteArray(BUFFER_SIZE) + private val stream = nativeHeap.alloc() + private val outputBuffer = ArrayList() + private val _isClosed = atomic(false) + + internal val isClosed + get() = _isClosed.value + + internal val availableForRead: Int + get() = outputBuffer.size + + init { + // Initialize deflate with gzip encoding + val initResult = deflateInit2_( + stream.ptr, + COMPRESSION_LEVEL, + Z_DEFLATED, + DEFAULT_WINDOW_BITS + GZIP_ENCODING, // Add 16 for gzip encoding + 8, // Default memory level + Z_DEFAULT_STRATEGY, + ZLIB_VERSION, + sizeOf().toInt(), + ) + + if (initResult != Z_OK) { + throw RuntimeException("Failed to initialize zlib deflate with error code $initResult: ${zError(initResult)!!.toKString()}") + } + } + + /** + * Update the compressor with [input] bytes + */ + fun update(input: ByteArray) = memScoped { + val inputPin = input.pin() + + stream.next_in = inputPin.addressOf(0).reinterpret() + stream.avail_in = input.size.toUInt() + + while (stream.avail_in > 0u) { + val outputPin = buffer.pin() + stream.next_out = outputPin.addressOf(0).reinterpret() + stream.avail_out = BUFFER_SIZE.toUInt() + + val deflateResult = deflate(stream.ptr, Z_NO_FLUSH) + if (deflateResult != Z_OK) { + throw RuntimeException("Deflate failed: $deflateResult") + } + + val bytesWritten = BUFFER_SIZE - stream.avail_out.toInt() + outputBuffer.addAll(buffer.take(bytesWritten)) + + outputPin.unpin() + } + + inputPin.unpin() + } + + /** + * Consume [count] gzip-compressed bytes. + */ + fun consume(count: Int): ByteArray { + if (count < 0) { + throw IllegalArgumentException("Requested bytes must be at least 0, got $count") + } + if (count > availableForRead) { + throw IllegalArgumentException("Requested more bytes than available, $count > ${outputBuffer.size}") + } + + val result = outputBuffer.take(count).toByteArray() + repeat(count) { outputBuffer.removeAt(0) } + return result + } + + /** + * Close the compressor, clean up all resources, and return the terminal sequence of bytes + * that represent the end of the gzip compression. + */ + fun close(): ByteArray { + if (isClosed) { return byteArrayOf() } + + memScoped { + var finished = false + + while (!finished) { + val outputPin = buffer.pin() + stream.next_out = outputPin.addressOf(0).reinterpret() + stream.avail_out = BUFFER_SIZE.toUInt() + + val deflateResult = deflate(stream.ptr, Z_FINISH) + if (deflateResult != Z_STREAM_END && deflateResult != Z_OK) { + throw RuntimeException("Deflate failed during finish: $deflateResult") + } + + val bytesWritten = BUFFER_SIZE - stream.avail_out.toInt() + outputBuffer.addAll(buffer.take(bytesWritten)) + + finished = deflateResult == Z_STREAM_END + outputPin.unpin() + } + + deflateEnd(stream.ptr) + nativeHeap.free(stream.ptr) + _isClosed.value = true + + return outputBuffer.toByteArray() + } + } +} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index 8ae22cd53e..787cce8c07 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -9,15 +9,6 @@ import aws.smithy.kotlin.runtime.io.GzipByteReadChannel import aws.smithy.kotlin.runtime.io.GzipSdkSource import aws.smithy.kotlin.runtime.io.SdkByteReadChannel import aws.smithy.kotlin.runtime.io.SdkSource -import kotlinx.cinterop.addressOf -import kotlinx.cinterop.alloc -import kotlinx.cinterop.memScoped -import kotlinx.cinterop.ptr -import kotlinx.cinterop.refTo -import kotlinx.cinterop.reinterpret -import kotlinx.cinterop.sizeOf -import kotlinx.cinterop.usePinned -import platform.zlib.* /** * The gzip compression algorithm. @@ -47,7 +38,9 @@ public actual class Gzip : CompressionAlgorithm { if (sourceBytes.isEmpty()) { stream } else { - val compressed = gzipCompressBytes(sourceBytes) + val compressed = GzipCompressor().apply { + update(sourceBytes) + }.close() object : ByteStream.Buffer() { override fun bytes(): ByteArray = compressed @@ -56,49 +49,3 @@ public actual class Gzip : CompressionAlgorithm { } } } - -internal fun gzipCompressBytes(bytes: ByteArray): ByteArray { - val compressedBuffer = UByteArray(bytes.size + 128) - memScoped { - val zStream = alloc().apply { - zalloc = null - zfree = null - opaque = null - } - - // Initialize the deflate context with gzip encoding - val initResult = deflateInit2_( - strm = zStream.ptr, - level = Z_BEST_COMPRESSION, - method = Z_DEFLATED, - windowBits = 31, // 15 (max window bits) + 16 (GZIP offset) - memLevel = 9, - strategy = Z_DEFAULT_STRATEGY, - version = ZLIB_VERSION, - stream_size = sizeOf().toInt(), - ) - if (initResult != Z_OK) { - throw IllegalStateException("Failed to initialize zlib with error code $initResult") - } - - try { - zStream.next_in = bytes.refTo(0).getPointer(memScope).reinterpret() - zStream.avail_in = bytes.size.toUInt() - - compressedBuffer.usePinned { pinnedBuffer -> - zStream.next_out = pinnedBuffer.addressOf(0) - zStream.avail_out = compressedBuffer.size.toUInt() - - val deflateResult = deflate(zStream.ptr, Z_FINISH) - if (deflateResult != Z_STREAM_END) { - throw IllegalStateException("Compression failed with error code $deflateResult") - } - } - - val compressedSize = compressedBuffer.size.toUInt() - zStream.avail_out - return compressedBuffer.copyOf(compressedSize.toInt()).toByteArray() - } finally { - deflateEnd(zStream.ptr) - } - } -} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt index 173515d455..b3057f2c89 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt @@ -8,11 +8,11 @@ import aws.smithy.kotlin.runtime.InternalApi import kotlinx.cinterop.addressOf import kotlinx.cinterop.alloc import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pin import kotlinx.cinterop.ptr -import kotlinx.cinterop.refTo import kotlinx.cinterop.reinterpret import kotlinx.cinterop.sizeOf -import kotlinx.cinterop.usePinned +import kotlinx.cinterop.toKString import platform.zlib.* /** @@ -24,14 +24,10 @@ public actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { return bytes } - val decompressedBuffer = UByteArray(bytes.size * 2) // Initial guess for decompressed size (may expand) + val decompressedBuffer = UByteArray(bytes.size * 2).pin() // Initial guess for decompressed size (may expand) memScoped { - val zStream = alloc().apply { - zalloc = null - zfree = null - opaque = null - } + val zStream = alloc() // Initialize the inflate context for gzip decoding val result = inflateInit2_( @@ -45,30 +41,33 @@ public actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { } try { - zStream.next_in = bytes.refTo(0).getPointer(memScope).reinterpret() + val bytesPinned = bytes.pin() + + zStream.next_in = bytesPinned.addressOf(0).getPointer(memScope).reinterpret() zStream.avail_in = bytes.size.toUInt() val output = mutableListOf() while (zStream.avail_in > 0u) { - decompressedBuffer.usePinned { pinnedBuffer -> - zStream.next_out = pinnedBuffer.addressOf(0) - zStream.avail_out = decompressedBuffer.size.toUInt() + zStream.next_out = decompressedBuffer.addressOf(0) + zStream.avail_out = decompressedBuffer.get().size.toUInt() - val inflateResult = inflate(zStream.ptr, Z_NO_FLUSH) + val inflateResult = inflate(zStream.ptr, Z_NO_FLUSH) - when (inflateResult) { - Z_OK, Z_STREAM_END -> { - val chunkSize = decompressedBuffer.size.toUInt() - zStream.avail_out - output.addAll(decompressedBuffer.copyOf(chunkSize.toInt())) - } - else -> throw IllegalStateException("Decompression failed with error code $inflateResult") + when (inflateResult) { + Z_OK, Z_STREAM_END -> { + val chunkSize = decompressedBuffer.get().size.toUInt() - zStream.avail_out + output.addAll(decompressedBuffer.get().copyOf(chunkSize.toInt())) } - - if (inflateResult == Z_STREAM_END) { - return@usePinned true + else -> { + throw IllegalStateException("Decompression failed with error code $inflateResult: ${zError(inflateResult)!!.toKString()}") } } + + if (inflateResult == Z_STREAM_END) { + break + } } + return output.toUByteArray().toByteArray() } finally { inflateEnd(zStream.ptr) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt index 2911844ed6..fd0d717e8a 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt @@ -5,7 +5,7 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.InternalApi -import aws.smithy.kotlin.runtime.compression.gzipCompressBytes +import aws.smithy.kotlin.runtime.compression.GzipCompressor /** * Wraps an [SdkByteReadChannel], compressing bytes read into GZIP format. @@ -13,11 +13,13 @@ import aws.smithy.kotlin.runtime.compression.gzipCompressBytes */ @InternalApi public actual class GzipByteReadChannel actual constructor(public val channel: SdkByteReadChannel) : SdkByteReadChannel { + private val compressor = GzipCompressor() + actual override val availableForRead: Int - get() = channel.availableForRead + get() = compressor.availableForRead actual override val isClosedForRead: Boolean - get() = channel.isClosedForRead + get() = channel.isClosedForRead && compressor.isClosed actual override val isClosedForWrite: Boolean get() = channel.isClosedForWrite @@ -28,18 +30,35 @@ public actual class GzipByteReadChannel actual constructor(public val channel: S actual override suspend fun read(sink: SdkBuffer, limit: Long): Long { require(limit >= 0L) if (limit == 0L) return 0L + if (compressor.isClosed) return -1L + + // If no compressed bytes are available, attempt to refill the compressor + if (compressor.availableForRead == 0 && !channel.isClosedForRead) { + val temp = SdkBuffer() + val rc = channel.read(temp, GzipCompressor.BUFFER_SIZE.toLong()) - val temp = SdkBuffer() - val rc = channel.read(temp, limit) + if (rc > 0) { + val input = temp.readByteArray(rc) + compressor.update(input) + } + } - if (rc <= 0) { - return rc + // If still no data is available and the channel is closed, we've hit EOF. Close the compressor and write the remaining bytes + if (compressor.availableForRead == 0 && channel.isClosedForRead) { + val terminationBytes = compressor.close() + sink.write(terminationBytes) + return terminationBytes.size.toLong() } - val compressed = gzipCompressBytes(temp.readByteArray()) + // Read compressed bytes from the compressor + val bytesToRead = minOf(limit, compressor.availableForRead.toLong()) + val compressed = compressor.consume(bytesToRead.toInt()) sink.write(compressed) return compressed.size.toLong() } - actual override fun cancel(cause: Throwable?): Boolean = channel.cancel(cause) + actual override fun cancel(cause: Throwable?): Boolean { + compressor.close() + return channel.cancel(cause) + } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt index ebdcd199c8..cbd38f524f 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt @@ -5,7 +5,7 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.InternalApi -import aws.smithy.kotlin.runtime.compression.gzipCompressBytes +import aws.smithy.kotlin.runtime.compression.GzipCompressor /** * Wraps an [SdkSource], compressing bytes read into GZIP format. @@ -13,23 +13,40 @@ import aws.smithy.kotlin.runtime.compression.gzipCompressBytes */ @InternalApi public actual class GzipSdkSource actual constructor(public val source: SdkSource) : SdkSource { + private val compressor = GzipCompressor() + actual override fun read(sink: SdkBuffer, limit: Long): Long { require(limit >= 0L) if (limit == 0L) return 0L + if (compressor.isClosed) return -1L + + // If no compressed bytes are available, attempt to refill the compressor + if (compressor.availableForRead == 0) { + val temp = SdkBuffer() + val rc = source.read(temp, GzipCompressor.BUFFER_SIZE.toLong()) - val temp = SdkBuffer() - val rc = source.read(temp, limit) + if (rc > 0) { + val input = temp.readByteArray(rc) + compressor.update(input) + } + } - if (rc <= 0L) { - return rc + // If still no data is available, we've hit EOF. Close the compressor and write the remaining bytes + if (compressor.availableForRead == 0) { + val terminationBytes = compressor.close() + sink.write(terminationBytes) + return terminationBytes.size.toLong() } - val compressed = gzipCompressBytes(temp.readByteArray()) + // Read compressed bytes from the compressor + val bytesToRead = minOf(limit, compressor.availableForRead.toLong()) + val compressed = compressor.consume(bytesToRead.toInt()) sink.write(compressed) return compressed.size.toLong() } actual override fun close() { + compressor.close() source.close() } } From 958e9f0893af22647bc4524f4f0e9ae9c3b2e08c Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Wed, 29 Jan 2025 15:55:52 -0500 Subject: [PATCH 39/58] ktlint --- .../http/interceptors/RequestCompressionInterceptorTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt index 6925b909a6..28b5c65e55 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt @@ -6,10 +6,10 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.IgnoreNative -import aws.smithy.kotlin.runtime.compression.decompressGzipBytes import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.compression.CompressionAlgorithm import aws.smithy.kotlin.runtime.compression.Gzip +import aws.smithy.kotlin.runtime.compression.decompressGzipBytes import aws.smithy.kotlin.runtime.http.* import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext import aws.smithy.kotlin.runtime.http.operation.newTestOperation From 45e455d5d207f426400dc026bc7477b7d439b497 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Wed, 29 Jan 2025 15:57:17 -0500 Subject: [PATCH 40/58] Revert comment change --- .../RequestCompressionInterceptorTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt index 28b5c65e55..34ee995c8c 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt @@ -59,7 +59,7 @@ class RequestCompressionInterceptorTest { return op.context.attributes[HttpOperationContext.HttpCallList].first() } - @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation + @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testCompressionThresholdTooHigh() = runTest { val payload = "bar" @@ -79,7 +79,7 @@ class RequestCompressionInterceptorTest { assertEquals(bytes, sentBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation + @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testCompression() = runTest { val payload = "bar" @@ -100,7 +100,7 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation + @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testSdkSource() = runTest { val payload = "bar" @@ -121,7 +121,7 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation + @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testSdkByteReadChannel() = runTest { val payload = "bar" @@ -142,7 +142,7 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation + @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testHeaderAlreadySet() = runTest { val payload = "bar" @@ -164,7 +164,7 @@ class RequestCompressionInterceptorTest { assertContentEquals(bytes, decompressedBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation + @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testNoSupportedAlgorithms() = runTest { val payload = "bar" @@ -184,7 +184,7 @@ class RequestCompressionInterceptorTest { assertEquals(bytes, sentBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation + @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation @Test fun testInvalidCompressionThreshold() = runTest { val payload = "bar" From d543e03af4cb95fac1b6a9c4fd61f608d59fcfa0 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Wed, 29 Jan 2025 15:57:44 -0500 Subject: [PATCH 41/58] Revert comment change --- .../http/interceptors/RequestCompressionInterceptorTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt index 34ee995c8c..725989fe98 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/RequestCompressionInterceptorTest.kt @@ -59,7 +59,7 @@ class RequestCompressionInterceptorTest { return op.context.attributes[HttpOperationContext.HttpCallList].first() } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testCompressionThresholdTooHigh() = runTest { val payload = "bar" @@ -79,7 +79,7 @@ class RequestCompressionInterceptorTest { assertEquals(bytes, sentBytes) } - @IgnoreNative // FIXME Re-enable after Kotlin/Native implementation + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testCompression() = runTest { val payload = "bar" From 29bb5ffec7ee6b5a0edced0391678d29af027665 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Wed, 29 Jan 2025 15:59:18 -0500 Subject: [PATCH 42/58] Remove consts --- .../smithy/kotlin/runtime/compression/GzipCompressor.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index 7f5a4b251a..58e9c96274 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -8,10 +8,6 @@ import kotlinx.atomicfu.atomic import kotlinx.cinterop.* import platform.zlib.* -private const val DEFAULT_WINDOW_BITS = 15 -private const val GZIP_ENCODING = 16 // Add this to windowBits for gzip encoding -private const val COMPRESSION_LEVEL = Z_BEST_COMPRESSION - /** * Streaming-style gzip compressor, implemented using zlib bindings */ @@ -35,9 +31,9 @@ internal class GzipCompressor { // Initialize deflate with gzip encoding val initResult = deflateInit2_( stream.ptr, - COMPRESSION_LEVEL, + Z_BEST_COMPRESSION, Z_DEFLATED, - DEFAULT_WINDOW_BITS + GZIP_ENCODING, // Add 16 for gzip encoding + 15 + 16, // Default windows bits (15) plus 16 for gzip encoding 8, // Default memory level Z_DEFAULT_STRATEGY, ZLIB_VERSION, From e08055e1369e59ebe9df6bccc397d725422df29a Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Wed, 29 Jan 2025 16:00:05 -0500 Subject: [PATCH 43/58] Remove outdated comment --- .../smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt index b3057f2c89..6015984fb3 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt @@ -24,7 +24,7 @@ public actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { return bytes } - val decompressedBuffer = UByteArray(bytes.size * 2).pin() // Initial guess for decompressed size (may expand) + val decompressedBuffer = UByteArray(bytes.size * 2).pin() memScoped { val zStream = alloc() From cc4535871c53f72bfb6d43bec3443a81a0192201 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Wed, 29 Jan 2025 16:01:44 -0500 Subject: [PATCH 44/58] ktlint --- .../aws/smithy/kotlin/runtime/compression/GzipCompressor.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index 58e9c96274..748d6c81d7 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -94,7 +94,9 @@ internal class GzipCompressor { * that represent the end of the gzip compression. */ fun close(): ByteArray { - if (isClosed) { return byteArrayOf() } + if (isClosed) { + return byteArrayOf() + } memScoped { var finished = false From e6357f92abd84dd2276a0f9ba27204be610d0068 Mon Sep 17 00:00:00 2001 From: Xinsong Cui Date: Thu, 30 Jan 2025 16:34:10 -0500 Subject: [PATCH 45/58] misc: add telemetry configuration to DefaultAwsSigner (#1226) * add telemetry provider configuration * lint * address pr reviews * add changelog --- .../d3ce7511-6fb2-4435-8f46-db724551b384.json | 5 ++++ .../api/aws-signing-default.api | 8 ++++++ .../auth/awssigning/DefaultAwsSigner.kt | 27 ++++++++++++++++--- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .changes/d3ce7511-6fb2-4435-8f46-db724551b384.json diff --git a/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json b/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json new file mode 100644 index 0000000000..aa89d9167e --- /dev/null +++ b/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json @@ -0,0 +1,5 @@ +{ + "id": "d3ce7511-6fb2-4435-8f46-db724551b384", + "type": "misc", + "description": "Add telemetry provider configuration to `DefaultAwsSigner`" +} \ No newline at end of file diff --git a/runtime/auth/aws-signing-default/api/aws-signing-default.api b/runtime/auth/aws-signing-default/api/aws-signing-default.api index 3b9535c403..e00e6d829a 100644 --- a/runtime/auth/aws-signing-default/api/aws-signing-default.api +++ b/runtime/auth/aws-signing-default/api/aws-signing-default.api @@ -1,4 +1,12 @@ +public final class aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerBuilder { + public fun ()V + public final fun build ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; + public final fun getTelemetryProvider ()Laws/smithy/kotlin/runtime/telemetry/TelemetryProvider; + public final fun setTelemetryProvider (Laws/smithy/kotlin/runtime/telemetry/TelemetryProvider;)V +} + public final class aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerKt { + public static final fun DefaultAwsSigner (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; public static final fun getDefaultAwsSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; } diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt index df613d31b0..791d8fce0a 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt @@ -4,8 +4,10 @@ */ package aws.smithy.kotlin.runtime.auth.awssigning +import aws.smithy.kotlin.runtime.ExperimentalApi import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.telemetry.TelemetryProvider import aws.smithy.kotlin.runtime.telemetry.logging.logger import aws.smithy.kotlin.runtime.time.TimestampFormat import kotlin.coroutines.coroutineContext @@ -13,13 +15,30 @@ import kotlin.coroutines.coroutineContext /** The default implementation of [AwsSigner] */ public val DefaultAwsSigner: AwsSigner = DefaultAwsSignerImpl() +/** Creates a customized instance of [AwsSigner] */ +@Suppress("ktlint:standard:function-naming") +public fun DefaultAwsSigner(block: DefaultAwsSignerBuilder.() -> Unit): AwsSigner = + DefaultAwsSignerBuilder().apply(block).build() + +/** A builder class for creating instances of [AwsSigner] using the default implementation */ +public class DefaultAwsSignerBuilder { + public var telemetryProvider: TelemetryProvider? = null + + public fun build(): AwsSigner = DefaultAwsSignerImpl( + telemetryProvider = telemetryProvider, + ) +} + +@OptIn(ExperimentalApi::class) internal class DefaultAwsSignerImpl( private val canonicalizer: Canonicalizer = Canonicalizer.Default, private val signatureCalculator: SignatureCalculator = SignatureCalculator.Default, private val requestMutator: RequestMutator = RequestMutator.Default, + private val telemetryProvider: TelemetryProvider? = null, ) : AwsSigner { override suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult { - val logger = coroutineContext.logger() + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() // TODO: implement SigV4a if (config.algorithm != AwsSigningAlgorithm.SIGV4) { @@ -52,7 +71,8 @@ internal class DefaultAwsSignerImpl( prevSignature: ByteArray, config: AwsSigningConfig, ): AwsSigningResult { - val logger = coroutineContext.logger() + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() val stringToSign = signatureCalculator.chunkStringToSign(chunkBody, prevSignature, config) logger.trace { "Chunk string to sign:\n$stringToSign" } @@ -70,7 +90,8 @@ internal class DefaultAwsSignerImpl( prevSignature: ByteArray, config: AwsSigningConfig, ): AwsSigningResult { - val logger = coroutineContext.logger() + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() // FIXME - can we share canonicalization code more than we are..., also this reduce is inefficient. // canonicalize the headers From 477201d25c39a224aa07940cf5fdbb7a1bf1e60f Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 16:40:50 -0500 Subject: [PATCH 46/58] Use `check`, simplify atomic with boolean --- .../runtime/compression/GzipCompressor.kt | 30 ++++++------------- .../kotlin/runtime/compression/GzipNative.kt | 2 +- .../runtime/io/GzipByteReadChannelNative.kt | 4 +-- .../kotlin/runtime/io/GzipSdkSourceNative.kt | 4 +-- 4 files changed, 14 insertions(+), 26 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index 748d6c81d7..b40b803d2d 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -4,7 +4,6 @@ */ package aws.smithy.kotlin.runtime.compression -import kotlinx.atomicfu.atomic import kotlinx.cinterop.* import platform.zlib.* @@ -19,10 +18,7 @@ internal class GzipCompressor { private val buffer = ByteArray(BUFFER_SIZE) private val stream = nativeHeap.alloc() private val outputBuffer = ArrayList() - private val _isClosed = atomic(false) - - internal val isClosed - get() = _isClosed.value + internal var isClosed = false internal val availableForRead: Int get() = outputBuffer.size @@ -40,9 +36,7 @@ internal class GzipCompressor { sizeOf().toInt(), ) - if (initResult != Z_OK) { - throw RuntimeException("Failed to initialize zlib deflate with error code $initResult: ${zError(initResult)!!.toKString()}") - } + check(initResult == Z_OK) { "Failed to initialize zlib deflate with error code $initResult: ${zError(initResult)!!.toKString()}" } } /** @@ -60,9 +54,7 @@ internal class GzipCompressor { stream.avail_out = BUFFER_SIZE.toUInt() val deflateResult = deflate(stream.ptr, Z_NO_FLUSH) - if (deflateResult != Z_OK) { - throw RuntimeException("Deflate failed: $deflateResult") - } + check(deflateResult == Z_OK) { "Deflate failed with error code $deflateResult" } val bytesWritten = BUFFER_SIZE - stream.avail_out.toInt() outputBuffer.addAll(buffer.take(bytesWritten)) @@ -77,11 +69,8 @@ internal class GzipCompressor { * Consume [count] gzip-compressed bytes. */ fun consume(count: Int): ByteArray { - if (count < 0) { - throw IllegalArgumentException("Requested bytes must be at least 0, got $count") - } - if (count > availableForRead) { - throw IllegalArgumentException("Requested more bytes than available, $count > ${outputBuffer.size}") + require(count in 0..availableForRead) { + "Count must be between 0 and $availableForRead, got $count" } val result = outputBuffer.take(count).toByteArray() @@ -90,10 +79,9 @@ internal class GzipCompressor { } /** - * Close the compressor, clean up all resources, and return the terminal sequence of bytes - * that represent the end of the gzip compression. + * Flush the compressor and return the terminal sequence of bytes that represent the end of the gzip compression. */ - fun close(): ByteArray { + fun flush(): ByteArray { if (isClosed) { return byteArrayOf() } @@ -108,7 +96,7 @@ internal class GzipCompressor { val deflateResult = deflate(stream.ptr, Z_FINISH) if (deflateResult != Z_STREAM_END && deflateResult != Z_OK) { - throw RuntimeException("Deflate failed during finish: $deflateResult") + throw RuntimeException("Deflate failed during finish with error code $deflateResult") } val bytesWritten = BUFFER_SIZE - stream.avail_out.toInt() @@ -120,7 +108,7 @@ internal class GzipCompressor { deflateEnd(stream.ptr) nativeHeap.free(stream.ptr) - _isClosed.value = true + isClosed = true return outputBuffer.toByteArray() } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index 787cce8c07..f892cbe2a6 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -40,7 +40,7 @@ public actual class Gzip : CompressionAlgorithm { } else { val compressed = GzipCompressor().apply { update(sourceBytes) - }.close() + }.flush() object : ByteStream.Buffer() { override fun bytes(): ByteArray = compressed diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt index fd0d717e8a..1fcbf9d2db 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt @@ -45,7 +45,7 @@ public actual class GzipByteReadChannel actual constructor(public val channel: S // If still no data is available and the channel is closed, we've hit EOF. Close the compressor and write the remaining bytes if (compressor.availableForRead == 0 && channel.isClosedForRead) { - val terminationBytes = compressor.close() + val terminationBytes = compressor.flush() sink.write(terminationBytes) return terminationBytes.size.toLong() } @@ -58,7 +58,7 @@ public actual class GzipByteReadChannel actual constructor(public val channel: S } actual override fun cancel(cause: Throwable?): Boolean { - compressor.close() + compressor.flush() return channel.cancel(cause) } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt index cbd38f524f..7334d06dcc 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt @@ -33,7 +33,7 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc // If still no data is available, we've hit EOF. Close the compressor and write the remaining bytes if (compressor.availableForRead == 0) { - val terminationBytes = compressor.close() + val terminationBytes = compressor.flush() sink.write(terminationBytes) return terminationBytes.size.toLong() } @@ -46,7 +46,7 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc } actual override fun close() { - compressor.close() + compressor.flush() source.close() } } From c8408a7bc65a817c50e7b9044de4a6d91cb77df8 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 16:59:28 -0500 Subject: [PATCH 47/58] Implement `Closeable` --- .../kotlin/runtime/compression/GzipCompressor.kt | 15 ++++++++++----- .../kotlin/runtime/compression/GzipNative.kt | 9 ++++++--- .../runtime/io/GzipByteReadChannelNative.kt | 6 ++++-- .../kotlin/runtime/io/GzipSdkSourceNative.kt | 6 ++++-- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index b40b803d2d..eff3b4e049 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -4,13 +4,14 @@ */ package aws.smithy.kotlin.runtime.compression +import aws.sdk.kotlin.crt.Closeable import kotlinx.cinterop.* import platform.zlib.* /** * Streaming-style gzip compressor, implemented using zlib bindings */ -internal class GzipCompressor { +internal class GzipCompressor : Closeable { companion object { internal const val BUFFER_SIZE = 16384 } @@ -106,11 +107,15 @@ internal class GzipCompressor { outputPin.unpin() } - deflateEnd(stream.ptr) - nativeHeap.free(stream.ptr) - isClosed = true - return outputBuffer.toByteArray() } } + + override fun close() { + if (isClosed) { return } + + deflateEnd(stream.ptr) + nativeHeap.free(stream.ptr) + isClosed = true + } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index f892cbe2a6..59a420f7e5 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -4,6 +4,7 @@ */ package aws.smithy.kotlin.runtime.compression +import aws.sdk.kotlin.crt.use import aws.smithy.kotlin.runtime.content.ByteStream import aws.smithy.kotlin.runtime.io.GzipByteReadChannel import aws.smithy.kotlin.runtime.io.GzipSdkSource @@ -38,9 +39,11 @@ public actual class Gzip : CompressionAlgorithm { if (sourceBytes.isEmpty()) { stream } else { - val compressed = GzipCompressor().apply { - update(sourceBytes) - }.flush() + val compressed = GzipCompressor().use { + it.apply { + update(sourceBytes) + }.flush() + } object : ByteStream.Buffer() { override fun bytes(): ByteArray = compressed diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt index 1fcbf9d2db..4fadeacbb9 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipByteReadChannelNative.kt @@ -47,7 +47,9 @@ public actual class GzipByteReadChannel actual constructor(public val channel: S if (compressor.availableForRead == 0 && channel.isClosedForRead) { val terminationBytes = compressor.flush() sink.write(terminationBytes) - return terminationBytes.size.toLong() + return terminationBytes.size.toLong().also { + compressor.close() + } } // Read compressed bytes from the compressor @@ -58,7 +60,7 @@ public actual class GzipByteReadChannel actual constructor(public val channel: S } actual override fun cancel(cause: Throwable?): Boolean { - compressor.flush() + compressor.close() return channel.cancel(cause) } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt index 7334d06dcc..39055dd4d9 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt @@ -35,7 +35,9 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc if (compressor.availableForRead == 0) { val terminationBytes = compressor.flush() sink.write(terminationBytes) - return terminationBytes.size.toLong() + return terminationBytes.size.toLong().also { + compressor.close() + } } // Read compressed bytes from the compressor @@ -46,7 +48,7 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc } actual override fun close() { - compressor.flush() + compressor.close() source.close() } } From 45316ddc7e92d5db14cde754b3f563e397d311a2 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 17:01:40 -0500 Subject: [PATCH 48/58] ByteStream.fromBytes --- .../src/aws/smithy/kotlin/runtime/compression/GzipNative.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index 59a420f7e5..77edbf9d92 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -45,9 +45,7 @@ public actual class Gzip : CompressionAlgorithm { }.flush() } - object : ByteStream.Buffer() { - override fun bytes(): ByteArray = compressed - } + ByteStream.fromBytes(compressed) } } } From 06621fa3ab1bdc13f7b178affd10ac9a2ab4a063 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 17:03:55 -0500 Subject: [PATCH 49/58] `const` --- .../kotlin/runtime/compression/GzipCompressor.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index eff3b4e049..b4a0112a78 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -8,6 +8,10 @@ import aws.sdk.kotlin.crt.Closeable import kotlinx.cinterop.* import platform.zlib.* +private const val DEFAULT_WINDOW_BITS = 15 // Default window bits +private const val WINDOW_BITS_GZIP_OFFSET = 16 // Gzip offset for window bits +private const val MEM_LEVEL = 8 // Default memory level + /** * Streaming-style gzip compressor, implemented using zlib bindings */ @@ -30,8 +34,8 @@ internal class GzipCompressor : Closeable { stream.ptr, Z_BEST_COMPRESSION, Z_DEFLATED, - 15 + 16, // Default windows bits (15) plus 16 for gzip encoding - 8, // Default memory level + DEFAULT_WINDOW_BITS + WINDOW_BITS_GZIP_OFFSET, // Default windows bits (15) plus 16 for gzip encoding + MEM_LEVEL, Z_DEFAULT_STRATEGY, ZLIB_VERSION, sizeOf().toInt(), @@ -112,7 +116,9 @@ internal class GzipCompressor : Closeable { } override fun close() { - if (isClosed) { return } + if (isClosed) { + return + } deflateEnd(stream.ptr) nativeHeap.free(stream.ptr) From 4b4c6283c9c99ab0583be06931355ee3f2f7f97d Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 17:05:09 -0500 Subject: [PATCH 50/58] Update `check` --- .../aws/smithy/kotlin/runtime/compression/GzipCompressor.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index b4a0112a78..976b033cdd 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -100,9 +100,7 @@ internal class GzipCompressor : Closeable { stream.avail_out = BUFFER_SIZE.toUInt() val deflateResult = deflate(stream.ptr, Z_FINISH) - if (deflateResult != Z_STREAM_END && deflateResult != Z_OK) { - throw RuntimeException("Deflate failed during finish with error code $deflateResult") - } + check(deflateResult == Z_OK || deflateResult == Z_STREAM_END) { "Deflate failed during finish with error code $deflateResult" } val bytesWritten = BUFFER_SIZE - stream.avail_out.toInt() outputBuffer.addAll(buffer.take(bytesWritten)) From b882663d9d3d69065fba6aea1e78c94f0e298a4d Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 10:36:28 -0500 Subject: [PATCH 51/58] Refactor to buffer output using `SdkByteChannel` --- .../runtime/compression/GzipCompressor.kt | 58 ++++++++++++------- .../kotlin/runtime/compression/GzipNative.kt | 11 ++-- .../kotlin/runtime/io/GzipSdkSourceNative.kt | 8 ++- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index 976b033cdd..8a3e90bdb6 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -5,8 +5,14 @@ package aws.smithy.kotlin.runtime.compression import aws.sdk.kotlin.crt.Closeable +import aws.smithy.kotlin.runtime.io.SdkByteChannel +import aws.smithy.kotlin.runtime.io.readFully +import aws.smithy.kotlin.runtime.io.write import kotlinx.cinterop.* import platform.zlib.* +import aws.smithy.kotlin.runtime.io.SdkBuffer +import aws.smithy.kotlin.runtime.io.readToByteArray +import aws.smithy.kotlin.runtime.io.use private const val DEFAULT_WINDOW_BITS = 15 // Default window bits private const val WINDOW_BITS_GZIP_OFFSET = 16 // Gzip offset for window bits @@ -20,13 +26,12 @@ internal class GzipCompressor : Closeable { internal const val BUFFER_SIZE = 16384 } - private val buffer = ByteArray(BUFFER_SIZE) private val stream = nativeHeap.alloc() - private val outputBuffer = ArrayList() + private val outputBuffer = SdkByteChannel() internal var isClosed = false internal val availableForRead: Int - get() = outputBuffer.size + get() = outputBuffer.availableForRead init { // Initialize deflate with gzip encoding @@ -47,14 +52,18 @@ internal class GzipCompressor : Closeable { /** * Update the compressor with [input] bytes */ - fun update(input: ByteArray) = memScoped { + suspend fun update(input: ByteArray) = memScoped { + check (!isClosed) { "Compressor is closed" } + val inputPin = input.pin() stream.next_in = inputPin.addressOf(0).reinterpret() stream.avail_in = input.size.toUInt() + val compressionBuffer = ByteArray(BUFFER_SIZE) + while (stream.avail_in > 0u) { - val outputPin = buffer.pin() + val outputPin = compressionBuffer.pin() stream.next_out = outputPin.addressOf(0).reinterpret() stream.avail_out = BUFFER_SIZE.toUInt() @@ -62,7 +71,7 @@ internal class GzipCompressor : Closeable { check(deflateResult == Z_OK) { "Deflate failed with error code $deflateResult" } val bytesWritten = BUFFER_SIZE - stream.avail_out.toInt() - outputBuffer.addAll(buffer.take(bytesWritten)) + outputBuffer.write(compressionBuffer, 0, bytesWritten) outputPin.unpin() } @@ -73,43 +82,48 @@ internal class GzipCompressor : Closeable { /** * Consume [count] gzip-compressed bytes. */ - fun consume(count: Int): ByteArray { + suspend fun consume(count: Int): ByteArray { + check (!isClosed) { "Compressor is closed" } require(count in 0..availableForRead) { "Count must be between 0 and $availableForRead, got $count" } - val result = outputBuffer.take(count).toByteArray() - repeat(count) { outputBuffer.removeAt(0) } - return result + return SdkBuffer().use { + outputBuffer.readFully(it, count.toLong()) + it.readToByteArray() + } } /** * Flush the compressor and return the terminal sequence of bytes that represent the end of the gzip compression. */ - fun flush(): ByteArray { - if (isClosed) { - return byteArrayOf() - } + suspend fun flush(): ByteArray { + check (!isClosed) { "Compressor is closed" } memScoped { - var finished = false + val compressionBuffer = ByteArray(BUFFER_SIZE) + var deflateResult: Int? = null + var outputLength = 0L - while (!finished) { - val outputPin = buffer.pin() + do { + val outputPin = compressionBuffer.pin() stream.next_out = outputPin.addressOf(0).reinterpret() stream.avail_out = BUFFER_SIZE.toUInt() - val deflateResult = deflate(stream.ptr, Z_FINISH) + deflateResult = deflate(stream.ptr, Z_FINISH) check(deflateResult == Z_OK || deflateResult == Z_STREAM_END) { "Deflate failed during finish with error code $deflateResult" } val bytesWritten = BUFFER_SIZE - stream.avail_out.toInt() - outputBuffer.addAll(buffer.take(bytesWritten)) + outputBuffer.write(compressionBuffer, 0, bytesWritten) - finished = deflateResult == Z_STREAM_END + outputLength += bytesWritten.toLong() outputPin.unpin() - } + } while (deflateResult != Z_STREAM_END) - return outputBuffer.toByteArray() + return SdkBuffer().use { + outputBuffer.readFully(it, outputLength) + it.readByteArray() + } } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index 77edbf9d92..f0e87bcee4 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -10,6 +10,7 @@ import aws.smithy.kotlin.runtime.io.GzipByteReadChannel import aws.smithy.kotlin.runtime.io.GzipSdkSource import aws.smithy.kotlin.runtime.io.SdkByteReadChannel import aws.smithy.kotlin.runtime.io.SdkSource +import kotlinx.coroutines.runBlocking /** * The gzip compression algorithm. @@ -39,10 +40,12 @@ public actual class Gzip : CompressionAlgorithm { if (sourceBytes.isEmpty()) { stream } else { - val compressed = GzipCompressor().use { - it.apply { - update(sourceBytes) - }.flush() + val compressed = runBlocking { + GzipCompressor().use { + it.apply { + update(sourceBytes) + }.flush() + } } ByteStream.fromBytes(compressed) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt index 39055dd4d9..2c9bceb6b0 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.compression.GzipCompressor +import kotlinx.coroutines.runBlocking /** * Wraps an [SdkSource], compressing bytes read into GZIP format. @@ -27,13 +28,14 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc if (rc > 0) { val input = temp.readByteArray(rc) - compressor.update(input) + runBlocking { compressor.update(input) } + } } // If still no data is available, we've hit EOF. Close the compressor and write the remaining bytes if (compressor.availableForRead == 0) { - val terminationBytes = compressor.flush() + val terminationBytes = runBlocking { compressor.flush() } sink.write(terminationBytes) return terminationBytes.size.toLong().also { compressor.close() @@ -42,7 +44,7 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc // Read compressed bytes from the compressor val bytesToRead = minOf(limit, compressor.availableForRead.toLong()) - val compressed = compressor.consume(bytesToRead.toInt()) + val compressed = runBlocking { compressor.consume(bytesToRead.toInt()) } sink.write(compressed) return compressed.size.toLong() } From 27949300ff9264a8deb2bb2f2d52980949c1c1ff Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 10:36:46 -0500 Subject: [PATCH 52/58] ktlint --- .../kotlin/runtime/compression/GzipCompressor.kt | 12 ++++++------ .../smithy/kotlin/runtime/io/GzipSdkSourceNative.kt | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index 8a3e90bdb6..d5994fab31 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -5,14 +5,14 @@ package aws.smithy.kotlin.runtime.compression import aws.sdk.kotlin.crt.Closeable +import aws.smithy.kotlin.runtime.io.SdkBuffer import aws.smithy.kotlin.runtime.io.SdkByteChannel import aws.smithy.kotlin.runtime.io.readFully +import aws.smithy.kotlin.runtime.io.readToByteArray +import aws.smithy.kotlin.runtime.io.use import aws.smithy.kotlin.runtime.io.write import kotlinx.cinterop.* import platform.zlib.* -import aws.smithy.kotlin.runtime.io.SdkBuffer -import aws.smithy.kotlin.runtime.io.readToByteArray -import aws.smithy.kotlin.runtime.io.use private const val DEFAULT_WINDOW_BITS = 15 // Default window bits private const val WINDOW_BITS_GZIP_OFFSET = 16 // Gzip offset for window bits @@ -53,7 +53,7 @@ internal class GzipCompressor : Closeable { * Update the compressor with [input] bytes */ suspend fun update(input: ByteArray) = memScoped { - check (!isClosed) { "Compressor is closed" } + check(!isClosed) { "Compressor is closed" } val inputPin = input.pin() @@ -83,7 +83,7 @@ internal class GzipCompressor : Closeable { * Consume [count] gzip-compressed bytes. */ suspend fun consume(count: Int): ByteArray { - check (!isClosed) { "Compressor is closed" } + check(!isClosed) { "Compressor is closed" } require(count in 0..availableForRead) { "Count must be between 0 and $availableForRead, got $count" } @@ -98,7 +98,7 @@ internal class GzipCompressor : Closeable { * Flush the compressor and return the terminal sequence of bytes that represent the end of the gzip compression. */ suspend fun flush(): ByteArray { - check (!isClosed) { "Compressor is closed" } + check(!isClosed) { "Compressor is closed" } memScoped { val compressionBuffer = ByteArray(BUFFER_SIZE) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt index 2c9bceb6b0..82333a414d 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt @@ -29,7 +29,6 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc if (rc > 0) { val input = temp.readByteArray(rc) runBlocking { compressor.update(input) } - } } From 755cc4b2b428560cdd10de481f5e5b409b9ea434 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 10:44:59 -0500 Subject: [PATCH 53/58] Refactor to use `asByteStream` --- .../aws/smithy/kotlin/runtime/content/ByteStream.kt | 11 ++++++++++- .../smithy/kotlin/runtime/compression/GzipNative.kt | 7 +++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/ByteStream.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/ByteStream.kt index 58f0fa1b4c..75fc36c00d 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/ByteStream.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/ByteStream.kt @@ -7,7 +7,6 @@ package aws.smithy.kotlin.runtime.content import aws.smithy.kotlin.runtime.io.* import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.IO import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -197,3 +196,13 @@ private fun SdkSource.toFlow(bufferSize: Long): Flow = flow { emit(sink.readByteArray()) } } + +/** + * Convert this [String] to a [ByteStream] + */ +public fun String.asByteStream(): ByteStream = ByteStream.fromString(this) + +/** + * Convert this [ByteArray] to a [ByteStream] + */ +public fun ByteArray.asByteStream(): ByteStream = ByteStream.fromBytes(this) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index f0e87bcee4..f23b8106ca 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.compression import aws.sdk.kotlin.crt.use import aws.smithy.kotlin.runtime.content.ByteStream +import aws.smithy.kotlin.runtime.content.asByteStream import aws.smithy.kotlin.runtime.io.GzipByteReadChannel import aws.smithy.kotlin.runtime.io.GzipSdkSource import aws.smithy.kotlin.runtime.io.SdkByteReadChannel @@ -40,15 +41,13 @@ public actual class Gzip : CompressionAlgorithm { if (sourceBytes.isEmpty()) { stream } else { - val compressed = runBlocking { + runBlocking { GzipCompressor().use { it.apply { update(sourceBytes) }.flush() - } + }.asByteStream() } - - ByteStream.fromBytes(compressed) } } } From f3b8e7f624644a2bd5505f45b69c9a8333741f48 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 11:04:24 -0500 Subject: [PATCH 54/58] Update Gzip ByteStream.Buffer compression --- .../smithy/kotlin/runtime/compression/GzipNative.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index f23b8106ca..ee7079447b 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -4,13 +4,14 @@ */ package aws.smithy.kotlin.runtime.compression -import aws.sdk.kotlin.crt.use import aws.smithy.kotlin.runtime.content.ByteStream import aws.smithy.kotlin.runtime.content.asByteStream import aws.smithy.kotlin.runtime.io.GzipByteReadChannel import aws.smithy.kotlin.runtime.io.GzipSdkSource import aws.smithy.kotlin.runtime.io.SdkByteReadChannel import aws.smithy.kotlin.runtime.io.SdkSource +import aws.smithy.kotlin.runtime.io.readToByteArray +import aws.smithy.kotlin.runtime.io.source import kotlinx.coroutines.runBlocking /** @@ -37,16 +38,12 @@ public actual class Gzip : CompressionAlgorithm { } is ByteStream.Buffer -> { - val sourceBytes = stream.bytes() - if (sourceBytes.isEmpty()) { + val bytes = stream.bytes() + if (bytes.isEmpty()) { stream } else { runBlocking { - GzipCompressor().use { - it.apply { - update(sourceBytes) - }.flush() - }.asByteStream() + GzipSdkSource(bytes.source()).readToByteArray().asByteStream() } } } From 9eea733e6cd8735e3267a2bbed6e9c60c22d0e7b Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 11:10:11 -0500 Subject: [PATCH 55/58] apiDump --- runtime/runtime-core/api/runtime-core.api | 2 ++ .../src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index e46e62bd0c..b864b68dce 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -469,6 +469,8 @@ public final class aws/smithy/kotlin/runtime/content/ByteStreamJVMKt { } public final class aws/smithy/kotlin/runtime/content/ByteStreamKt { + public static final fun asByteStream (Ljava/lang/String;)Laws/smithy/kotlin/runtime/content/ByteStream; + public static final fun asByteStream ([B)Laws/smithy/kotlin/runtime/content/ByteStream; public static final fun cancel (Laws/smithy/kotlin/runtime/content/ByteStream;)V public static final fun decodeToString (Laws/smithy/kotlin/runtime/content/ByteStream;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun toByteArray (Laws/smithy/kotlin/runtime/content/ByteStream;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index d5994fab31..0c20185677 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -39,7 +39,7 @@ internal class GzipCompressor : Closeable { stream.ptr, Z_BEST_COMPRESSION, Z_DEFLATED, - DEFAULT_WINDOW_BITS + WINDOW_BITS_GZIP_OFFSET, // Default windows bits (15) plus 16 for gzip encoding + DEFAULT_WINDOW_BITS + WINDOW_BITS_GZIP_OFFSET, MEM_LEVEL, Z_DEFAULT_STRATEGY, ZLIB_VERSION, From a4f7ba6804e61bfe52d450e3145f5a6892a3f648 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 11:11:53 -0500 Subject: [PATCH 56/58] Use upload-artifact@v4 --- .github/workflows/continuous-integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 3187339faf..365a70b22b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -73,7 +73,7 @@ jobs: - name: Save Test Reports if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-reports-${{ matrix.os }} path: '**/build/reports' @@ -107,7 +107,7 @@ jobs: - name: Save Test Reports if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-reports-${{ matrix.os }} path: '**/build/reports' From bc3221d92083e0bbb0f6df3d5b388125906373c2 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 10:43:34 -0500 Subject: [PATCH 57/58] Refactor to use SdkBuffer --- .../runtime/compression/GzipCompressor.kt | 25 ++++++------------- .../kotlin/runtime/compression/GzipNative.kt | 8 +++--- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt index 0c20185677..954cfb3bd5 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -6,11 +6,6 @@ package aws.smithy.kotlin.runtime.compression import aws.sdk.kotlin.crt.Closeable import aws.smithy.kotlin.runtime.io.SdkBuffer -import aws.smithy.kotlin.runtime.io.SdkByteChannel -import aws.smithy.kotlin.runtime.io.readFully -import aws.smithy.kotlin.runtime.io.readToByteArray -import aws.smithy.kotlin.runtime.io.use -import aws.smithy.kotlin.runtime.io.write import kotlinx.cinterop.* import platform.zlib.* @@ -27,11 +22,11 @@ internal class GzipCompressor : Closeable { } private val stream = nativeHeap.alloc() - private val outputBuffer = SdkByteChannel() + private val outputBuffer = SdkBuffer() internal var isClosed = false internal val availableForRead: Int - get() = outputBuffer.availableForRead + get() = outputBuffer.size.toInt() init { // Initialize deflate with gzip encoding @@ -52,7 +47,7 @@ internal class GzipCompressor : Closeable { /** * Update the compressor with [input] bytes */ - suspend fun update(input: ByteArray) = memScoped { + fun update(input: ByteArray) = memScoped { check(!isClosed) { "Compressor is closed" } val inputPin = input.pin() @@ -82,22 +77,19 @@ internal class GzipCompressor : Closeable { /** * Consume [count] gzip-compressed bytes. */ - suspend fun consume(count: Int): ByteArray { + fun consume(count: Int): ByteArray { check(!isClosed) { "Compressor is closed" } require(count in 0..availableForRead) { "Count must be between 0 and $availableForRead, got $count" } - return SdkBuffer().use { - outputBuffer.readFully(it, count.toLong()) - it.readToByteArray() - } + return outputBuffer.readByteArray(count.toLong()) } /** * Flush the compressor and return the terminal sequence of bytes that represent the end of the gzip compression. */ - suspend fun flush(): ByteArray { + fun flush(): ByteArray { check(!isClosed) { "Compressor is closed" } memScoped { @@ -120,10 +112,7 @@ internal class GzipCompressor : Closeable { outputPin.unpin() } while (deflateResult != Z_STREAM_END) - return SdkBuffer().use { - outputBuffer.readFully(it, outputLength) - it.readByteArray() - } + return outputBuffer.readByteArray(outputLength) } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt index ee7079447b..f82274317a 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipNative.kt @@ -10,9 +10,9 @@ import aws.smithy.kotlin.runtime.io.GzipByteReadChannel import aws.smithy.kotlin.runtime.io.GzipSdkSource import aws.smithy.kotlin.runtime.io.SdkByteReadChannel import aws.smithy.kotlin.runtime.io.SdkSource -import aws.smithy.kotlin.runtime.io.readToByteArray +import aws.smithy.kotlin.runtime.io.buffer import aws.smithy.kotlin.runtime.io.source -import kotlinx.coroutines.runBlocking +import aws.smithy.kotlin.runtime.io.use /** * The gzip compression algorithm. @@ -42,8 +42,8 @@ public actual class Gzip : CompressionAlgorithm { if (bytes.isEmpty()) { stream } else { - runBlocking { - GzipSdkSource(bytes.source()).readToByteArray().asByteStream() + GzipSdkSource(bytes.source()).use { + it.buffer().readByteArray().asByteStream() } } } From 002ae428206984e33053a9c92d746a0f13a0fce1 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 10:44:27 -0500 Subject: [PATCH 58/58] Remove stray `runBlocking` calls --- .../aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt index 82333a414d..39055dd4d9 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/GzipSdkSourceNative.kt @@ -6,7 +6,6 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.compression.GzipCompressor -import kotlinx.coroutines.runBlocking /** * Wraps an [SdkSource], compressing bytes read into GZIP format. @@ -28,13 +27,13 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc if (rc > 0) { val input = temp.readByteArray(rc) - runBlocking { compressor.update(input) } + compressor.update(input) } } // If still no data is available, we've hit EOF. Close the compressor and write the remaining bytes if (compressor.availableForRead == 0) { - val terminationBytes = runBlocking { compressor.flush() } + val terminationBytes = compressor.flush() sink.write(terminationBytes) return terminationBytes.size.toLong().also { compressor.close() @@ -43,7 +42,7 @@ public actual class GzipSdkSource actual constructor(public val source: SdkSourc // Read compressed bytes from the compressor val bytesToRead = minOf(limit, compressor.availableForRead.toLong()) - val compressed = runBlocking { compressor.consume(bytesToRead.toInt()) } + val compressed = compressor.consume(bytesToRead.toInt()) sink.write(compressed) return compressed.size.toLong() }