diff --git a/.brazil.json b/.brazil.json index a347a892a7..ad60e171a6 100644 --- a/.brazil.json +++ b/.brazil.json @@ -14,6 +14,7 @@ "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", + "org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.*": "KotlinxDatetimeJvm-0.x", "software.amazon.smithy:smithy-aws-traits:1.*": "Maven-software-amazon-smithy_smithy-aws-traits-1.x", "software.amazon.smithy:smithy-aws-iam-traits:1.*": "Maven-software-amazon-smithy_smithy-aws-iam-traits-1.x", 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/.changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json b/.changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json new file mode 100644 index 0000000000..297f7e603f --- /dev/null +++ b/.changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json @@ -0,0 +1,5 @@ +{ + "id": "4820850c-8916-47f5-a7e1-8880e6a00d22", + "type": "bugfix", + "description": "Fix errors in equality checks for `CaseInsensitiveMap` which affect `Headers` and `ValuesMap` implementations" +} \ No newline at end of file diff --git a/.changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json b/.changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json new file mode 100644 index 0000000000..42302b920d --- /dev/null +++ b/.changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json @@ -0,0 +1,5 @@ +{ + "id": "c0040355-ffdc-4813-80e9-baf859ef02b9", + "type": "bugfix", + "description": "fix: correct hash code calculation for case-insensitive map entries" +} \ No newline at end of file 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/.github/actions/setup-build/action.yml b/.github/actions/setup-build/action.yml new file mode 100644 index 0000000000..5e72c0c6d5 --- /dev/null +++ b/.github/actions/setup-build/action.yml @@ -0,0 +1,49 @@ +name: Setup Build +description: > + Checkout repositories and build dependencies + +runs: + using: composite + steps: + - name: Extract aws-kotlin-repo-tools version + working-directory: ./smithy-kotlin + shell: bash + run: | + export AWS_KOTLIN_REPO_TOOLS_VERSION=$(grep '^aws-kotlin-repo-tools-version' ./gradle/libs.versions.toml | sed -E 's/.*= "(.*)"/\1/') + echo "Using aws-kotlin-repo-tools version $AWS_KOTLIN_REPO_TOOLS_VERSION" + echo "aws_kotlin_repo_tools_version=$AWS_KOTLIN_REPO_TOOLS_VERSION" >> $GITHUB_ENV + + - name: Checkout aws-kotlin-repo-tools + uses: actions/checkout@v4 + with: + path: 'aws-kotlin-repo-tools' + repository: 'awslabs/aws-kotlin-repo-tools' + ref: ${{ env.aws_kotlin_repo_tools_version }} + sparse-checkout: | + .github + + - name: Checkout aws-crt-kotlin + uses: ./aws-kotlin-repo-tools/.github/actions/checkout-head + with: + # checkout aws-crt-kotlin as a sibling which will automatically make it an included build + path: 'aws-crt-kotlin' + repository: 'awslabs/aws-crt-kotlin' + submodules: 'true' + + # Cache the Kotlin/Native toolchain based on the input Kotlin version from version catalog + # see https://kotlinlang.org/docs/native-improving-compilation-time.html + - name: Cache Kotlin Native toolchain + uses: actions/cache@v4 + with: + path: | + ~/.konan + key: ${{ runner.os }}-konan-${{ hashFiles('gradle/libs.versions.toml') }} + restore-keys: | + ${{ runner.os }}-konan- + + - name: Configure JDK + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: 17 + cache: 'gradle' \ No newline at end of file diff --git a/.github/scripts/run-container-test.py b/.github/scripts/run-container-test.py new file mode 100755 index 0000000000..56d9be34d3 --- /dev/null +++ b/.github/scripts/run-container-test.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +""" +Run precompiled Kotlin/Native test binaries in a Docker container for a specific Linux distribution and architecture. + +This requires Docker multiarch support, see https://docs.docker.com/build/building/multi-platform/ and https://github.com/multiarch/qemu-user-static +In GitHub we use a provided action for this: https://github.com/docker/setup-qemu-action + +Locally you would need to run one of: + +`docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes` + +OR + +`docker run --privileged --rm tonistiigi/binfmt --install all` +""" + +import argparse +import os +import subprocess +import shlex +import shutil + +VERBOSE = False + +DISTRO_TO_IMAGE_NAME = { + "ubuntu-22.04": "public.ecr.aws/lts/ubuntu:22.04_stable", + "al2023": "public.ecr.aws/amazonlinux/amazonlinux:2023", + "al2": "public.ecr.aws/amazonlinux/amazonlinux:2" +} + +DOCKER_PLATFORM_BY_ARCH = { + "x64": "linux/amd64", + "arm64": "linux/arm64" +} + + +def vprint(message): + global VERBOSE + if VERBOSE: + print(message) + + +def running_in_github_action(): + """ + Test if currently running in a GitHub action or running locally + :return: True if running in GH, False otherwise + """ + return "GITHUB_WORKFLOW" in os.environ + + +def shell(command, cwd=None, check=True, capture_output=False): + """ + Run a command + :param command: command to run + :param cwd: the current working directory to change to before executing the command + :param check: flag indicating if the status code should be checked. When true an exception will be + thrown if the command exits with a non-zero exit status. + :returns: the subprocess CompletedProcess output + """ + vprint(f"running `{command}`") + return subprocess.run(command, shell=True, check=check, cwd=cwd, capture_output=capture_output) + + +def oci_executable(): + """ + Attempt to find the OCI container executor used to build and run docker containers + """ + oci_exe = os.environ.get('OCI_EXE') + if oci_exe is not None: + return oci_exe + + executors = ['finch', 'podman', 'docker'] + + for exe in executors: + if shutil.which(exe) is not None: + return exe + + print("cannot find container executor") + exit(1) + + +def run_docker_test(opts): + """ + Run a docker test for a precompiled Kotlin/Native binary + + :param opts: the parsed command line options + """ + platform = DOCKER_PLATFORM_BY_ARCH[opts.arch] + oci_exe = oci_executable() + + test_bin_dir = os.path.abspath(opts.test_bin_dir) + image_name = DISTRO_TO_IMAGE_NAME[opts.distro] + path_to_exe = f'./linux{opts.arch.capitalize()}/debugTest/test.kexe' + + cmd = [ + oci_exe, + 'run', + '--rm', + f'-v{test_bin_dir}:/test', + ] + if not opts.no_system_certs: + cmd.append(f'-v/etc/ssl:/etc/ssl') + + cmd.extend( + [ + '-w/test', + '-e DEBIAN_FRONTEND=noninteractive', + '--platform', + platform, + image_name, + path_to_exe, + ] + ) + + cmd = shlex.join(cmd) + print(cmd) + shell(cmd) + + +def create_cli(): + parser = argparse.ArgumentParser( + prog="run-container-test", + description="Run cross platform test binaries in a container", + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument("-v", "--verbose", help="enable verbose output", action="store_true") + + parser.add_argument("--distro", required=True, choices=DISTRO_TO_IMAGE_NAME.keys(), help="the distribution name to run the task on") + parser.add_argument("--arch", required=True, choices=DOCKER_PLATFORM_BY_ARCH.keys(), help="the architecture to use") + parser.add_argument("--test-bin-dir", required=True, help="the path to the test binary directory root") + parser.add_argument("--no-system-certs", action='store_true', help="disable mounting system certificates into the container") + + return parser + + +def main(): + cli = create_cli() + opts = cli.parse_args() + if opts.verbose: + global VERBOSE + VERBOSE = True + + run_docker_test(opts) + + +if __name__ == '__main__': + main() diff --git a/.github/workflows/artifact-size-metrics.yml b/.github/workflows/artifact-size-metrics.yml index 03571e1d56..8c22752455 100644 --- a/.github/workflows/artifact-size-metrics.yml +++ b/.github/workflows/artifact-size-metrics.yml @@ -44,33 +44,41 @@ jobs: steps: - name: Checkout Sources uses: actions/checkout@v4 - - name: Configure JDK - uses: actions/setup-java@v3 with: - distribution: 'corretto' - java-version: 17 - cache: 'gradle' + path: smithy-kotlin + + - name: Setup build + uses: ./smithy-kotlin/.github/actions/setup-build + - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.CI_AWS_ROLE_ARN }} aws-region: us-west-2 + - name: Configure Gradle uses: aws/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + with: + working-directory: smithy-kotlin + - name: Generate Artifact Size Metrics - run: ./gradlew artifactSizeMetrics + run: ./gradlew -Paws.kotlin.native=false artifactSizeMetrics + working-directory: smithy-kotlin + - name: Analyze Artifact Size Metrics run: ./gradlew analyzeArtifactSizeMetrics + working-directory: smithy-kotlin - name: Show Results uses: aws/aws-kotlin-repo-tools/.github/actions/artifact-size-metrics/show-results@main - name: Evaluate if: ${{ !contains(github.event.pull_request.labels.*.name, 'acknowledge-artifact-size-increase') }} + working-directory: smithy-kotlin run: | cd build/reports/metrics cat has-significant-change.txt | grep false || { echo An artifact increased in size by more than allowed or a new artifact was created. echo If this is expected please add the 'acknowledge-artifact-size-increase' label to this pull request. exit 1 - } \ No newline at end of file + } diff --git a/.github/workflows/changelog-verification.yml b/.github/workflows/changelog-verification.yml index f3e6a70f47..bc9b81bded 100644 --- a/.github/workflows/changelog-verification.yml +++ b/.github/workflows/changelog-verification.yml @@ -4,7 +4,7 @@ permissions: id-token: write on: - pull_request: + pull_request_target: types: [ opened, synchronize, reopened, labeled, unlabeled ] branches: - main @@ -22,4 +22,4 @@ jobs: aws-region: us-west-2 - name: Verify changelog - uses: aws/aws-kotlin-repo-tools/.github/actions/changelog-verification@main \ No newline at end of file + uses: aws/aws-kotlin-repo-tools/.github/actions/changelog-verification@main diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index a72ae44016..9dd930cbdf 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -33,6 +33,12 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + with: + path: 'smithy-kotlin' + + - name: Setup build + uses: ./smithy-kotlin/.github/actions/setup-build + - name: Configure JDK uses: actions/setup-java@v3 with: @@ -41,10 +47,112 @@ jobs: cache: 'gradle' - name: Configure Gradle uses: aws/aws-kotlin-repo-tools/.github/actions/configure-gradle@main - - name: Test + with: + working-directory: 'smithy-kotlin' + + - name: Build and Test on JVM + working-directory: ./smithy-kotlin + shell: bash + run: | + ./gradlew -Paws.kotlin.native=false -Ptest.java.version=${{ matrix.java-version }} jvmTest --stacktrace + + # macos-14 build and test for targets: jvm, macoArm64, iosSimulatorArm64, watchosSimulatorArm65, tvosSimulatorArm64 + # macos-13 build and test for targets: jvm, macoX64, iosX64, tvosX64, watchosX64 + macos: + strategy: + fail-fast: false + matrix: + os: [macos-14, macos-13] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + path: 'smithy-kotlin' + + - name: Setup build + uses: ./smithy-kotlin/.github/actions/setup-build + + - name: Build and Test on Apple platforms + working-directory: ./smithy-kotlin shell: bash run: | - ./gradlew -Ptest.java.version=${{ matrix.java-version }} jvmTest --stacktrace + # 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 + + ./gradlew apiCheck + ./gradlew build + + - name: Save Test Reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-reports-${{ matrix.os }} + path: '**/build/reports' + + # build and test for targets: jvm, linuxX64 + # cross compile for: linuxX64, linuxArm64 + # TODO - add mingw as cross compile target + linux: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + path: 'smithy-kotlin' + + - name: Configure Gradle + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + with: + working-directory: 'smithy-kotlin' + + - name: Setup build + uses: ./smithy-kotlin/.github/actions/setup-build + + - name: Configure CRT Docker Images + run: | + ./aws-crt-kotlin/docker-images/build-all.sh + + - name: Build and Test on Linux with Cross-Compile + working-directory: ./smithy-kotlin + 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 + ./gradlew apiCheck + ./gradlew build + + - name: Save Test Reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-reports-${{ matrix.os }} + path: '**/build/reports' + + # windows JVM + windows: + runs-on: windows-2022 + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + path: 'smithy-kotlin' + + - name: Setup build + uses: ./smithy-kotlin/.github/actions/setup-build + + - name: Build and Test on Windows JVM + working-directory: ./smithy-kotlin + run: | + ./gradlew apiCheck + ./gradlew -P"aws.kotlin.native=false" build + + - name: Save Test Reports + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-reports-windows + path: '**/build/reports' all-platforms: runs-on: ${{ matrix.os }} @@ -55,20 +163,33 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + with: + path: 'smithy-kotlin' + + - name: Setup build + uses: ./smithy-kotlin/.github/actions/setup-build + - name: Configure JDK uses: actions/setup-java@v3 with: distribution: 'corretto' java-version: 17 cache: 'gradle' + - name: Configure Gradle uses: aws/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + with: + working-directory: 'smithy-kotlin' + - name: Test + working-directory: ./smithy-kotlin shell: bash run: | - echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties - ./gradlew apiCheck - ./gradlew test jvmTest + # 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 + ./gradlew -Paws.kotlin.native=false apiCheck + ./gradlew -Paws.kotlin.native=false test jvmTest + - name: Save Test Reports if: failure() uses: actions/upload-artifact@v4 @@ -81,19 +202,30 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + with: + path: 'smithy-kotlin' + + - name: Setup build + uses: ./smithy-kotlin/.github/actions/setup-build + - name: Configure JDK uses: actions/setup-java@v3 with: distribution: 'corretto' java-version: 17 cache: 'gradle' + - name: Configure Gradle uses: aws/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + with: + working-directory: 'smithy-kotlin' + - name: Test + working-directory: ./smithy-kotlin shell: bash run: | - ./gradlew publishToMavenLocal - ./gradlew testAllProtocols + ./gradlew -Paws.kotlin.native=false publishToMavenLocal + ./gradlew -Paws.kotlin.native=false testAllProtocols downstream: runs-on: ubuntu-latest @@ -102,6 +234,10 @@ jobs: uses: actions/checkout@v4 with: path: 'smithy-kotlin' + + - name: Setup build + uses: ./smithy-kotlin/.github/actions/setup-build + - name: Checkout tools uses: actions/checkout@v4 with: @@ -110,31 +246,30 @@ jobs: ref: '0.2.3' sparse-checkout: | .github + - name: Checkout aws-sdk-kotlin uses: ./aws-kotlin-repo-tools/.github/actions/checkout-head with: # smithy-kotlin is checked out as a sibling dir which will automatically make it an included build path: 'aws-sdk-kotlin' - repository: 'aws/aws-sdk-kotlin' + repository: 'awslabs/aws-sdk-kotlin' + - name: Configure Gradle - smithy-kotlin - uses: aws/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main with: working-directory: ./smithy-kotlin + - name: Configure Gradle - aws-sdk-kotlin - uses: aws/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main with: working-directory: ./aws-sdk-kotlin - - name: Configure JDK - uses: actions/setup-java@v3 - with: - distribution: 'corretto' - java-version: 17 - cache: 'gradle' + - name: Build and Test aws-sdk-kotlin downstream + working-directory: ./smithy-kotlin run: | # TODO - JVM only cd $GITHUB_WORKSPACE/smithy-kotlin - ./gradlew --parallel publishToMavenLocal + ./gradlew --parallel -Paws.kotlin.native=false publishToMavenLocal SMITHY_KOTLIN_RUNTIME_VERSION=$(grep sdkVersion= gradle.properties | cut -d = -f 2) SMITHY_KOTLIN_CODEGEN_VERSION=$(grep codegenVersion= gradle.properties | cut -d = -f 2) cd $GITHUB_WORKSPACE/aws-sdk-kotlin @@ -145,6 +280,6 @@ jobs: # smithy-kotlin should be sed -i "s/smithy-kotlin-runtime-version = .*$/smithy-kotlin-runtime-version = \"$SMITHY_KOTLIN_RUNTIME_VERSION\"/" gradle/libs.versions.toml sed -i "s/smithy-kotlin-codegen-version = .*$/smithy-kotlin-codegen-version = \"$SMITHY_KOTLIN_CODEGEN_VERSION\"/" gradle/libs.versions.toml - ./gradlew --parallel publishToMavenLocal - ./gradlew test jvmTest - ./gradlew testAllProtocols \ No newline at end of file + ./gradlew --parallel -Paws.kotlin.native=false publishToMavenLocal + ./gradlew -Paws.kotlin.native=false test jvmTest + ./gradlew -Paws.kotlin.native=false testAllProtocols diff --git a/.github/workflows/kat-transform.yml b/.github/workflows/kat-transform.yml index 93fb0c8b34..6f2dfae691 100644 --- a/.github/workflows/kat-transform.yml +++ b/.github/workflows/kat-transform.yml @@ -29,6 +29,9 @@ jobs: with: path: 'smithy-kotlin' + - name: Setup build + uses: ./smithy-kotlin/.github/actions/setup-build + - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -43,13 +46,6 @@ jobs: - name: Setup kat uses: aws/aws-kotlin-repo-tools/.github/actions/setup-kat@main - - name: Configure JDK - uses: actions/setup-java@v3 - with: - distribution: 'corretto' - java-version: 17 - cache: 'gradle' - - name: Build working-directory: ./smithy-kotlin shell: bash @@ -58,8 +54,8 @@ jobs: ls -lsa kat bump-version # Bump from `vNext-SNAPSHOT` to `vNext`. kat transform only works on non-SNAPSHOT versions kat bump-version --property codegenVersion - ./gradlew build - ./gradlew publishAllPublicationsToTestLocalRepository + ./gradlew -Paws.kotlin.native=false build + ./gradlew -Paws.kotlin.native=false publishAllPublicationsToTestLocalRepository - name: Transform working-directory: ./smithy-kotlin @@ -75,4 +71,4 @@ jobs: exit 1 fi - echo "Transformation succeeded!" \ No newline at end of file + echo "Transformation succeeded!" diff --git a/.gitignore b/.gitignore index 99ff20f122..824314f693 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ gradle-app.setting # MacOS .DS_Store + +*.cinteropLibraries.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b57263573d..abd6e50a04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -234,3 +234,25 @@ If you discover a potential security issue in this project we ask that you notif See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. + +## Kotlin/Native +Support for Kotlin/Native is in active development. The following sections cover how to effectively test your work. + +### Linux/Unix + +#### Testing Different Linux Distros and Architectures + +1. Build the test executable(s) for the architecture(s) you want to test. + +```sh +# build specific arch +./gradlew linuxX64TestBinaries +``` + +2. Use the `run-container-test.py` helper script to execute tests locally. Change the directory as needed. + +```sh +OCI_EXE=docker python3 .github/scripts/run-container-test.py --distro al2 --arch x64 --test-bin-dir ./runtime/runtime-core/build/bin +``` + +See the usage/help for different distributions provided: `python3 .github/scripts/run-container.py -h` diff --git a/buildSrc/src/main/kotlin/dokka-convention.gradle.kts b/buildSrc/src/main/kotlin/dokka-convention.gradle.kts index 06f6593bdb..7169bb7e87 100644 --- a/buildSrc/src/main/kotlin/dokka-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/dokka-convention.gradle.kts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import kotlin.text.set - plugins { id("org.jetbrains.dokka") } diff --git a/gradle.properties b/gradle.properties index 5909f84ca5..c90e8aaef3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,7 @@ kotlin.code.style=official kotlin.incremental.js=true kotlin.incremental.multiplatform=true kotlin.mpp.stability.nowarn=true +kotlin.mpp.enableCInteropCommonization=true kotlin.native.ignoreDisabledTargets=true # atomicfu @@ -20,4 +21,4 @@ codegenVersion=0.35.9-SNAPSHOT # FIXME Remove after Dokka 2.0 Gradle plugin is stable org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled -org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true \ No newline at end of file +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 40a1338394..f7bcf7886e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin-version = "2.2.0" dokka-version = "2.0.0" -aws-kotlin-repo-tools-version = "0.4.50" +aws-kotlin-repo-tools-version = "0.4.51" # libs coroutines-version = "1.10.2" @@ -16,6 +16,8 @@ slf4j-v1x-version = "1.7.36" crt-kotlin-version = "0.10.3" micrometer-version = "1.15.2" binary-compatibility-validator-version = "0.18.1" +kotlin-multiplatform-bignum-version = "0.3.10" +kotlinx-datetime-version = "0.6.1" # codegen smithy-version = "1.61.0" @@ -64,6 +66,7 @@ slf4j-api-v1x = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-v1x-versi slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j-version" } crt-kotlin = { module = "aws.sdk.kotlin.crt:aws-crt-kotlin", version.ref = "crt-kotlin-version" } micrometer-core = { module = "io.micrometer:micrometer-core", version.ref = "micrometer-version" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime-version" } smithy-codegen-core = { module = "software.amazon.smithy:smithy-codegen-core", version.ref = "smithy-version" } smithy-cli = { module = "software.amazon.smithy:smithy-cli", version.ref = "smithy-version" } @@ -94,14 +97,18 @@ docker-transport-zerodep = { module = "com.github.docker-java:docker-java-transp ktor-http-cio = { module = "io.ktor:ktor-http-cio", version.ref = "ktor-version" } ktor-utils = { module = "io.ktor:ktor-utils", version.ref = "ktor-version" } ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor-version" } +ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor-version" } ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor-version" } ktor-server-jetty-jakarta = { module = "io.ktor:ktor-server-jetty-jakarta", version.ref = "ktor-version" } ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor-version" } ktor-network-tls-certificates = { module = "io.ktor:ktor-network-tls-certificates", version.ref = "ktor-version" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-version" } kaml = { module = "com.charleskorn.kaml:kaml", version.ref = "kaml-version" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup-version" } +kotlin-multiplatform-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "kotlin-multiplatform-bignum-version" } + [plugins] dokka = { id = "org.jetbrains.dokka", version.ref = "dokka-version"} kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" } 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 bc14e6689b..a78bd0143a 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 @@ -3,14 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -/* -* 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.Headers import aws.smithy.kotlin.runtime.http.HttpMethod import aws.smithy.kotlin.runtime.http.request.HttpRequest 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 880f4f1d91..50d38aeb5c 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 @@ -3,10 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -/* -* 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 diff --git a/runtime/auth/aws-signing-crt/api/aws-signing-crt.api b/runtime/auth/aws-signing-crt/api/aws-signing-crt.api index ed1934cac9..39dc9fdf45 100644 --- a/runtime/auth/aws-signing-crt/api/aws-signing-crt.api +++ b/runtime/auth/aws-signing-crt/api/aws-signing-crt.api @@ -1,4 +1,4 @@ -public final class aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner : aws/smithy/kotlin/runtime/auth/awssigning/AwsSigner { +public final class aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner : aws/sdk/kotlin/crt/WithCrt, aws/smithy/kotlin/runtime/auth/awssigning/AwsSigner { public static final field INSTANCE Laws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner; public fun sign (Laws/smithy/kotlin/runtime/http/request/HttpRequest;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun signChunk ([B[BLaws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/runtime/auth/aws-signing-crt/build.gradle.kts b/runtime/auth/aws-signing-crt/build.gradle.kts index 4c67282762..5dddb3f45f 100644 --- a/runtime/auth/aws-signing-crt/build.gradle.kts +++ b/runtime/auth/aws-signing-crt/build.gradle.kts @@ -8,14 +8,14 @@ extra["moduleName"] = "aws.smithy.kotlin.runtime.auth.awssigning.crt" kotlin { sourceSets { - jvmMain { + jvmAndNativeMain { dependencies { api(project(":runtime:auth:aws-signing-common")) implementation(project(":runtime:crt-util")) } } - jvmTest { + jvmAndNativeTest { dependencies { implementation(project(":runtime:auth:aws-signing-tests")) } diff --git a/runtime/auth/aws-signing-crt/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner.kt b/runtime/auth/aws-signing-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner.kt similarity index 98% rename from runtime/auth/aws-signing-crt/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner.kt rename to runtime/auth/aws-signing-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner.kt index c0069c1aca..ac039d519b 100644 --- a/runtime/auth/aws-signing-crt/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner.kt +++ b/runtime/auth/aws-signing-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsSigner.kt @@ -4,6 +4,7 @@ */ package aws.smithy.kotlin.runtime.auth.awssigning.crt +import aws.sdk.kotlin.crt.WithCrt import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awssigning.* import aws.smithy.kotlin.runtime.crt.toSignableCrtRequest @@ -24,7 +25,7 @@ import aws.sdk.kotlin.crt.http.Headers as CrtHeaders private const val S3_EXPRESS_HEADER_NAME = "X-Amz-S3session-Token" -public object CrtAwsSigner : AwsSigner { +public object CrtAwsSigner : AwsSigner, WithCrt() { override suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult { val isUnsigned = config.hashSpecification is HashSpecification.UnsignedPayload val isAwsChunked = request.headers.contains("Content-Encoding", "aws-chunked") diff --git a/runtime/auth/aws-signing-crt/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsChunkedByteReadChannelTest.kt b/runtime/auth/aws-signing-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsChunkedByteReadChannelTest.kt similarity index 100% rename from runtime/auth/aws-signing-crt/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsChunkedByteReadChannelTest.kt rename to runtime/auth/aws-signing-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsChunkedByteReadChannelTest.kt diff --git a/runtime/auth/aws-signing-crt/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsChunkedSourceTest.kt b/runtime/auth/aws-signing-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsChunkedSourceTest.kt similarity index 100% rename from runtime/auth/aws-signing-crt/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsChunkedSourceTest.kt rename to runtime/auth/aws-signing-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtAwsChunkedSourceTest.kt diff --git a/runtime/auth/aws-signing-crt/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtBasicSigningTest.kt b/runtime/auth/aws-signing-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtBasicSigningTest.kt similarity index 100% rename from runtime/auth/aws-signing-crt/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtBasicSigningTest.kt rename to runtime/auth/aws-signing-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtBasicSigningTest.kt diff --git a/runtime/auth/aws-signing-crt/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtSigningSuiteTest.kt b/runtime/auth/aws-signing-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtSigningSuiteTest.kt similarity index 64% rename from runtime/auth/aws-signing-crt/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtSigningSuiteTest.kt rename to runtime/auth/aws-signing-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtSigningSuiteTest.kt index 1b7ed92a07..f312bf09a0 100644 --- a/runtime/auth/aws-signing-crt/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtSigningSuiteTest.kt +++ b/runtime/auth/aws-signing-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/auth/awssigning/crt/CrtSigningSuiteTest.kt @@ -9,10 +9,4 @@ import aws.smithy.kotlin.runtime.auth.awssigning.tests.SigningSuiteTestBase class CrtSigningSuiteTest : SigningSuiteTestBase() { override val signer: AwsSigner = CrtAwsSigner - - override val disabledTests = super.disabledTests + setOf( - // FIXME - Signature mismatch possibly related to https://github.com/awslabs/aws-crt-java/pull/419. Needs - // investigation. - "get-utf8", - ) } 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 e00e6d829a..15ca42c07e 100644 --- a/runtime/auth/aws-signing-default/api/aws-signing-default.api +++ b/runtime/auth/aws-signing-default/api/aws-signing-default.api @@ -5,7 +5,7 @@ public final class aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerBui public final fun setTelemetryProvider (Laws/smithy/kotlin/runtime/telemetry/TelemetryProvider;)V } -public final class aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerKt { +public final class aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerJVMKt { 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/build.gradle.kts b/runtime/auth/aws-signing-default/build.gradle.kts index 22e20d39fd..71781c66c7 100644 --- a/runtime/auth/aws-signing-default/build.gradle.kts +++ b/runtime/auth/aws-signing-default/build.gradle.kts @@ -25,5 +25,11 @@ kotlin { all { languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi") } + + nativeMain { + dependencies { + implementation(project(":runtime:auth:aws-signing-crt")) + } + } } } 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 85896349b1..df5d2076b9 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,136 +4,5 @@ */ 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 - /** 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, - ) -} - -private val AwsSigningAlgorithm.signatureCalculator - get() = when (this) { - AwsSigningAlgorithm.SIGV4 -> SignatureCalculator.SigV4 - AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> SignatureCalculator.SigV4a - } - -@OptIn(ExperimentalApi::class) -internal class DefaultAwsSignerImpl( - private val canonicalizer: Canonicalizer = Canonicalizer.Default, - private val requestMutator: RequestMutator = RequestMutator.Default, - private val telemetryProvider: TelemetryProvider? = null, -) : AwsSigner { - override suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult { - val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") - ?: coroutineContext.logger() - - val canonical = canonicalizer.canonicalRequest(request, config) - if (config.logRequest) { - logger.trace { "Canonical request:\n${canonical.requestString}" } - } - - val signatureCalculator = config.algorithm.signatureCalculator - - val stringToSign = signatureCalculator.stringToSign(canonical.requestString, config) - logger.trace { "String to sign:\n$stringToSign" } - - val signingKey = signatureCalculator.signingKey(config) - - val signature = signatureCalculator.calculate(signingKey, stringToSign) - logger.debug { "Calculated signature: $signature" } - - val signedRequest = requestMutator.appendAuth(config, canonical, signature) - - return AwsSigningResult(signedRequest, signature.encodeToByteArray()) - } - - override suspend fun signChunk( - chunkBody: ByteArray, - prevSignature: ByteArray, - config: AwsSigningConfig, - ): AwsSigningResult { - val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") - ?: coroutineContext.logger() - - val signatureCalculator = config.algorithm.signatureCalculator - - val stringToSign = signatureCalculator.chunkStringToSign(chunkBody, prevSignature, config) - logger.trace { "Chunk string to sign:\n$stringToSign" } - - val signingKey = signatureCalculator.signingKey(config) - - val signature = signatureCalculator.calculate(signingKey, stringToSign) - logger.debug { "Calculated chunk signature: $signature" } - - return AwsSigningResult(Unit, signature.encodeToByteArray()) - } - - override suspend fun signChunkTrailer( - trailingHeaders: Headers, - prevSignature: ByteArray, - config: AwsSigningConfig, - ): AwsSigningResult { - val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") - ?: coroutineContext.logger() - - val signatureCalculator = config.algorithm.signatureCalculator - - // FIXME - can we share canonicalization code more than we are..., also this reduce is inefficient. - // canonicalize the headers - val trailingHeadersBytes = trailingHeaders.entries().sortedBy { e -> e.key.lowercase() } - .map { e -> - buildString { - append(e.key.lowercase()) - append(":") - append(e.value.joinToString(",") { v -> v.trim() }) - append("\n") - }.encodeToByteArray() - }.reduce { acc, bytes -> acc + bytes } - - val stringToSign = signatureCalculator.chunkTrailerStringToSign(trailingHeadersBytes, prevSignature, config) - logger.trace { "Chunk trailer string to sign:\n$stringToSign" } - - val signingKey = signatureCalculator.signingKey(config) - - val signature = signatureCalculator.calculate(signingKey, stringToSign) - logger.debug { "Calculated chunk signature: $signature" } - - return AwsSigningResult(Unit, signature.encodeToByteArray()) - } -} - -/** - * Formats a credential scope consisting of a signing date, region (SigV4 only), service, and a signature type - */ -internal val AwsSigningConfig.credentialScope: String - get() = run { - val signingDate = signingDate.format(TimestampFormat.ISO_8601_CONDENSED_DATE) - return when (algorithm) { - AwsSigningAlgorithm.SIGV4 -> "$signingDate/$region/$service/aws4_request" - AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> "$signingDate/$service/aws4_request" - } - } - -/** - * Formats the value for a credential header/parameter - */ -internal fun credentialValue(config: AwsSigningConfig): String = - "${config.credentials.accessKeyId}/${config.credentialScope}" +public expect val DefaultAwsSigner: AwsSigner diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/RequestMutator.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/RequestMutator.kt index 0c1882cd54..ee67e7833b 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/RequestMutator.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/RequestMutator.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.auth.awssigning import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.time.TimestampFormat /** * An object that can mutate requests to include signing attributes. @@ -55,3 +56,21 @@ internal class DefaultRequestMutator : RequestMutator { return canonical.request.build() } } + +/** + * Formats a credential scope consisting of a signing date, region (SigV4 only), service, and a signature type + */ +internal val AwsSigningConfig.credentialScope: String + get() { + val signingDate = signingDate.format(TimestampFormat.ISO_8601_CONDENSED_DATE) + return when (algorithm) { + AwsSigningAlgorithm.SIGV4 -> "$signingDate/$region/$service/aws4_request" + AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> "$signingDate/$service/aws4_request" + } + } + +/** + * Formats the value for a credential header/parameter + */ +internal fun credentialValue(config: AwsSigningConfig): String = + "${config.credentials.accessKeyId}/${config.credentialScope}" diff --git a/runtime/auth/aws-signing-default/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsChunkedSourceTest.kt b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsChunkedSourceTest.kt similarity index 100% rename from runtime/auth/aws-signing-default/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsChunkedSourceTest.kt rename to runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsChunkedSourceTest.kt diff --git a/runtime/auth/aws-signing-default/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerJVM.kt b/runtime/auth/aws-signing-default/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerJVM.kt new file mode 100644 index 0000000000..ea1f4723d6 --- /dev/null +++ b/runtime/auth/aws-signing-default/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerJVM.kt @@ -0,0 +1,120 @@ +/* + * 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.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 kotlin.coroutines.coroutineContext + +/** The default implementation of [AwsSigner] */ +public actual 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, + ) +} + +private val AwsSigningAlgorithm.signatureCalculator + get() = when (this) { + AwsSigningAlgorithm.SIGV4 -> SignatureCalculator.SigV4 + AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> SignatureCalculator.SigV4a + } + +@OptIn(ExperimentalApi::class) +internal class DefaultAwsSignerImpl( + private val canonicalizer: Canonicalizer = Canonicalizer.Default, + private val requestMutator: RequestMutator = RequestMutator.Default, + private val telemetryProvider: TelemetryProvider? = null, +) : AwsSigner { + override suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult { + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() + + val canonical = canonicalizer.canonicalRequest(request, config) + if (config.logRequest) { + logger.trace { "Canonical request:\n${canonical.requestString}" } + } + + val signatureCalculator = config.algorithm.signatureCalculator + + val stringToSign = signatureCalculator.stringToSign(canonical.requestString, config) + logger.trace { "String to sign:\n$stringToSign" } + + val signingKey = signatureCalculator.signingKey(config) + + val signature = signatureCalculator.calculate(signingKey, stringToSign) + logger.debug { "Calculated signature: $signature" } + + val signedRequest = requestMutator.appendAuth(config, canonical, signature) + + return AwsSigningResult(signedRequest, signature.encodeToByteArray()) + } + + override suspend fun signChunk( + chunkBody: ByteArray, + prevSignature: ByteArray, + config: AwsSigningConfig, + ): AwsSigningResult { + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() + + val signatureCalculator = config.algorithm.signatureCalculator + + val stringToSign = signatureCalculator.chunkStringToSign(chunkBody, prevSignature, config) + logger.trace { "Chunk string to sign:\n$stringToSign" } + + val signingKey = signatureCalculator.signingKey(config) + + val signature = signatureCalculator.calculate(signingKey, stringToSign) + logger.debug { "Calculated chunk signature: $signature" } + + return AwsSigningResult(Unit, signature.encodeToByteArray()) + } + + override suspend fun signChunkTrailer( + trailingHeaders: Headers, + prevSignature: ByteArray, + config: AwsSigningConfig, + ): AwsSigningResult { + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() + + val signatureCalculator = config.algorithm.signatureCalculator + + // FIXME - can we share canonicalization code more than we are..., also this reduce is inefficient. + // canonicalize the headers + val trailingHeadersBytes = trailingHeaders.entries().sortedBy { e -> e.key.lowercase() } + .map { e -> + buildString { + append(e.key.lowercase()) + append(":") + append(e.value.joinToString(",") { v -> v.trim() }) + append("\n") + }.encodeToByteArray() + }.reduce { acc, bytes -> acc + bytes } + + val stringToSign = signatureCalculator.chunkTrailerStringToSign(trailingHeadersBytes, prevSignature, config) + logger.trace { "Chunk trailer string to sign:\n$stringToSign" } + + val signingKey = signatureCalculator.signingKey(config) + + val signature = signatureCalculator.calculate(signingKey, stringToSign) + logger.debug { "Calculated chunk signature: $signature" } + + return AwsSigningResult(Unit, signature.encodeToByteArray()) + } +} diff --git a/runtime/auth/aws-signing-default/native/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerNative.kt b/runtime/auth/aws-signing-default/native/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerNative.kt new file mode 100644 index 0000000000..0b9335ec1d --- /dev/null +++ b/runtime/auth/aws-signing-default/native/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerNative.kt @@ -0,0 +1,10 @@ +/* + * 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.awssigning.crt.CrtAwsSigner + +/** The default implementation of [AwsSigner] */ +public actual val DefaultAwsSigner: AwsSigner = CrtAwsSigner diff --git a/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/AwsChunkedSourceTestBase.kt b/runtime/auth/aws-signing-tests/common/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/AwsChunkedSourceTestBase.kt similarity index 100% rename from runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/AwsChunkedSourceTestBase.kt rename to runtime/auth/aws-signing-tests/common/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/AwsChunkedSourceTestBase.kt diff --git a/runtime/auth/http-auth-aws/build.gradle.kts b/runtime/auth/http-auth-aws/build.gradle.kts index dfcc0c7da3..e6c8236526 100644 --- a/runtime/auth/http-auth-aws/build.gradle.kts +++ b/runtime/auth/http-auth-aws/build.gradle.kts @@ -26,7 +26,7 @@ kotlin { } } - jvmTest { + jvmAndNativeTest { dependencies { implementation(project(":runtime:auth:aws-signing-crt")) } diff --git a/runtime/auth/http-auth-aws/jvm/test/AwsHttpSignerTestBaseJvm.kt b/runtime/auth/http-auth-aws/jvmAndNative/test/AwsHttpSignerTestBaseJvmAndNative.kt similarity index 100% rename from runtime/auth/http-auth-aws/jvm/test/AwsHttpSignerTestBaseJvm.kt rename to runtime/auth/http-auth-aws/jvmAndNative/test/AwsHttpSignerTestBaseJvmAndNative.kt diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 9da74f02a8..158856a208 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -3,9 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ import aws.sdk.kotlin.gradle.dsl.configurePublishing -import aws.sdk.kotlin.gradle.kmp.* -import org.gradle.kotlin.dsl.apply +import aws.sdk.kotlin.gradle.kmp.configureKmpTargets +import aws.sdk.kotlin.gradle.kmp.kotlin +import aws.sdk.kotlin.gradle.kmp.needsKmpConfigured import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest plugins { `dokka-convention` @@ -15,6 +18,9 @@ plugins { val sdkVersion: String by project +// Apply KMP configuration from build plugin +configureKmpTargets() + // capture locally - scope issue with custom KMP plugin val libraries = libs @@ -47,19 +53,25 @@ subprojects { } } - named("jvmTest") { + findByName("jvmTest")?.run { dependencies { implementation(libraries.kotlinx.coroutines.debug) implementation(libraries.kotest.assertions.core.jvm) } } + + all { + languageSettings.optIn("kotlin.RequiresOptIn") + } } - } - kotlin.sourceSets.all { - // Allow subprojects to use internal APIs - // See https://kotlinlang.org/docs/reference/opt-in-requirements.html#opting-in-to-using-api - listOf("kotlin.RequiresOptIn").forEach { languageSettings.optIn(it) } + targets.withType().all { + listOf("${name}Main", "${name}Test").forEach { + this@kotlin.sourceSets.findByName(it)?.apply { + languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") + } + } + } } tasks.withType { @@ -73,10 +85,64 @@ subprojects { tasks.withType { compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") + + // FIXME When building LinuxX64 on AL2 the linker inclues a bunch of dynamic links to unavailable versions + // of zlib. The below workaround forces the linker to statically link zlib but it's a hack because the + // linker will still dynamically link zlib (although the executable will no longer fail at runtime due to + // link resolution failures). The correct solution for this is probably containerized builds similar to + // what we do in aws-crt-kotlin. The following compiler args were helpful in debugging this issue: + // * Enable verbose compiler output : -verbose + // * Increase verbosity during the compiler's linker phase : -Xverbose-phases=Linker + // * Enable verbose linker output from gold : -linker-option --verbose + if (target.contains("linux", ignoreCase = true)) { + freeCompilerArgs.addAll( + listOf( + "-linker-option", // The subsequent argument is for the linker + "-Bstatic", // Enable static linking for the libraries that follow + "-linker-option", // The subsequent argument is for the linker + "-lz", // Link zlib statically (because of -Bstatic above) + "-linker-option", // The subsequent argument is for the linker + "-Bdynamic", // Restore dynamic linking, which is the default + ), + ) + } + } + } + + /* + FIXME iOS simulator tests fail on GitHub CI with "Bad or unknown session": + > Task :runtime:runtime-core:linkDebugTestIosSimulatorArm64 + java.lang.IllegalStateException: You have standalone simulator tests run mode disabled and tests have failed to run. + + The problem can be that you have not booted the required device or have configured the task to a different simulator. Please check the task output and its device configuration. + > Task :runtime:runtime-core:iosSimulatorArm64Test + If you are sure that your setup is correct, please file an issue: https://kotl.in/issue + An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405): + Process spawn via launchd failed because device is not booted. + Underlying error (domain=com.apple.SimLaunchHostService.RequestError, code=3): + Bad or unknown session: com.apple.CoreSimulator.SimDevice.C120BDE1-C108-4759-842F-7D82B4E71E8C + */ + tasks.withType { + enabled = false + } + + tasks.withType { + if (this is Test) { + useJUnitPlatform() + } + + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = true + showStackTraces = true + showExceptions = true + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL } } } +// configureIosSimulatorTasks() + val excludeFromDocumentation = listOf( ":runtime:testing", ":runtime:smithy-test", diff --git a/runtime/crt-util/build.gradle.kts b/runtime/crt-util/build.gradle.kts index ddc57d11a9..f14fb5c439 100644 --- a/runtime/crt-util/build.gradle.kts +++ b/runtime/crt-util/build.gradle.kts @@ -11,14 +11,22 @@ extra["moduleName"] = "aws.smithy.kotlin.runtime.crt" kotlin { sourceSets { - jvmMain { + commonMain { + dependencies { + api(project(":runtime:runtime-core")) + api(libs.crt.kotlin) + } + } + + jvmAndNativeMain { dependencies { api(project(":runtime:runtime-core")) api(libs.crt.kotlin) api(project(":runtime:protocol:http")) } } - jvmTest { + + jvmAndNativeTest { dependencies { implementation(libs.kotlinx.coroutines.test) } diff --git a/runtime/crt-util/common/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStream.kt b/runtime/crt-util/common/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStream.kt new file mode 100644 index 0000000000..e08f9f0d1f --- /dev/null +++ b/runtime/crt-util/common/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStream.kt @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.crt + +import aws.sdk.kotlin.crt.io.MutableBuffer +import aws.smithy.kotlin.runtime.io.SdkBuffer + +/** + * write as much of [outgoing] to [dest] as possible + */ +internal expect fun transferRequestBody(outgoing: SdkBuffer, dest: MutableBuffer): Int diff --git a/runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamJvm.kt b/runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamJvm.kt new file mode 100644 index 0000000000..cb2e691167 --- /dev/null +++ b/runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamJvm.kt @@ -0,0 +1,12 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.crt + +import aws.sdk.kotlin.crt.io.MutableBuffer +import aws.smithy.kotlin.runtime.io.SdkBuffer +import aws.smithy.kotlin.runtime.io.read + +internal actual fun transferRequestBody(outgoing: SdkBuffer, dest: MutableBuffer) = outgoing.read(dest.buffer) diff --git a/runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/Http.kt b/runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/Http.kt similarity index 100% rename from runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/Http.kt rename to runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/Http.kt diff --git a/runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStream.kt b/runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStream.kt similarity index 96% rename from runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStream.kt rename to runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStream.kt index 4a3e609808..040ba85459 100644 --- a/runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStream.kt +++ b/runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStream.kt @@ -18,11 +18,6 @@ import kotlin.time.Duration.Companion.milliseconds private val POLLING_DELAY = 100.milliseconds -/** - * write as much of [outgoing] to [dest] as possible - */ -internal fun transferRequestBody(outgoing: SdkBuffer, dest: MutableBuffer) = outgoing.read(dest.buffer) - /** * Implement's [HttpRequestBodyStream] which proxies an SDK request body channel [SdkByteReadChannel] */ diff --git a/runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/SdkDefaultIO.kt b/runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/SdkDefaultIO.kt similarity index 68% rename from runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/SdkDefaultIO.kt rename to runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/SdkDefaultIO.kt index cdcceaf596..269dcdb5ee 100644 --- a/runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/SdkDefaultIO.kt +++ b/runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/SdkDefaultIO.kt @@ -14,33 +14,39 @@ private const val DEFAULT_EVENT_LOOP_THREAD_COUNT: Int = 1 /** * Default (CRT) IO used by the SDK when not configured manually/directly */ +@Deprecated("This API is no longer used by the SDK and will be removed in version 1.6.x") @InternalApi public object SdkDefaultIO { /** * The default event loop group to run IO on */ + @Deprecated("This API is no longer used by the SDK and will be removed in version 1.6.x") public val EventLoop: EventLoopGroup by lazy { - // TODO - can we register shutdown in appropriate runtimes (e.g. jvm: addShutdown, native: atexit(), etc) when/if these lazy block(s) run? EventLoopGroup(DEFAULT_EVENT_LOOP_THREAD_COUNT) } /** * The default host resolver to resolve DNS queries with */ + @Deprecated("This API is no longer used by the SDK and will be removed in version 1.6.x") public val HostResolver: HostResolver by lazy { + @Suppress("DEPRECATION") HostResolver(EventLoop) } /** * The default client bootstrap */ + @Deprecated("This API is no longer used by the SDK and will be removed in version 1.6.x") public val ClientBootstrap: ClientBootstrap by lazy { + @Suppress("DEPRECATION") ClientBootstrap(EventLoop, HostResolver) } /** * The default TLS context */ + @Deprecated("This API is no longer used by the SDK and will be removed in version 1.6.x") public val TlsContext: TlsContext by lazy { TlsContext(TlsContextOptions.defaultClient()) } diff --git a/runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/SdkSourceBodyStream.kt b/runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/SdkSourceBodyStream.kt similarity index 100% rename from runtime/crt-util/jvm/src/aws/smithy/kotlin/runtime/crt/SdkSourceBodyStream.kt rename to runtime/crt-util/jvmAndNative/src/aws/smithy/kotlin/runtime/crt/SdkSourceBodyStream.kt diff --git a/runtime/crt-util/jvm/test/aws/smithy/kotlin/runtime/crt/HttpTest.kt b/runtime/crt-util/jvmAndNative/test/aws/smithy/kotlin/runtime/crt/HttpTest.kt similarity index 100% rename from runtime/crt-util/jvm/test/aws/smithy/kotlin/runtime/crt/HttpTest.kt rename to runtime/crt-util/jvmAndNative/test/aws/smithy/kotlin/runtime/crt/HttpTest.kt diff --git a/runtime/crt-util/jvm/test/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamTest.kt b/runtime/crt-util/jvmAndNative/test/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamTest.kt similarity index 100% rename from runtime/crt-util/jvm/test/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamTest.kt rename to runtime/crt-util/jvmAndNative/test/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamTest.kt diff --git a/runtime/crt-util/jvm/test/aws/smithy/kotlin/runtime/crt/SdkSourceBodyStreamTest.kt b/runtime/crt-util/jvmAndNative/test/aws/smithy/kotlin/runtime/crt/SdkSourceBodyStreamTest.kt similarity index 100% rename from runtime/crt-util/jvm/test/aws/smithy/kotlin/runtime/crt/SdkSourceBodyStreamTest.kt rename to runtime/crt-util/jvmAndNative/test/aws/smithy/kotlin/runtime/crt/SdkSourceBodyStreamTest.kt diff --git a/runtime/crt-util/native/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamNative.kt b/runtime/crt-util/native/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamNative.kt new file mode 100644 index 0000000000..e27805bbf6 --- /dev/null +++ b/runtime/crt-util/native/src/aws/smithy/kotlin/runtime/crt/ReadChannelBodyStreamNative.kt @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.crt + +import aws.sdk.kotlin.crt.io.MutableBuffer +import aws.smithy.kotlin.runtime.io.SdkBuffer + +internal actual fun transferRequestBody(outgoing: SdkBuffer, dest: MutableBuffer): Int { + val length = minOf(outgoing.size, dest.writeRemaining.toLong()) + if (length <= 0) return 0 + return dest.write(outgoing.readByteArray(length)) +} diff --git a/runtime/observability/logging-crt/build.gradle.kts b/runtime/observability/logging-crt/build.gradle.kts new file mode 100644 index 0000000000..b59d7b81d8 --- /dev/null +++ b/runtime/observability/logging-crt/build.gradle.kts @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +description = "Logging provider based on CRT" +extra["displayName"] = "Smithy :: Kotlin :: Observability :: CRT" +extra["moduleName"] = "aws.smithy.kotlin.runtime.telemetry.logging.crt" + +kotlin { + sourceSets { + nativeMain { + dependencies { + api(project(":runtime:observability:telemetry-api")) + api(libs.crt.kotlin) + } + } + } +} diff --git a/runtime/observability/logging-crt/native/src/aws/smithy/kotlin/runtime/telemetry/logging/crt/CrtLogRecordBuilder.kt b/runtime/observability/logging-crt/native/src/aws/smithy/kotlin/runtime/telemetry/logging/crt/CrtLogRecordBuilder.kt new file mode 100644 index 0000000000..67415ed59b --- /dev/null +++ b/runtime/observability/logging-crt/native/src/aws/smithy/kotlin/runtime/telemetry/logging/crt/CrtLogRecordBuilder.kt @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.telemetry.logging.crt + +import aws.smithy.kotlin.runtime.telemetry.logging.LogLevel +import aws.smithy.kotlin.runtime.telemetry.logging.LogRecordBuilder +import aws.smithy.kotlin.runtime.telemetry.logging.MessageSupplier + +public class CrtLogRecordBuilder( + private val delegate: CrtLogger, + private val level: LogLevel, +) : LogRecordBuilder { + private var cause: Throwable? = null + private var msg: (() -> String)? = null + + override fun setCause(ex: Throwable) { + cause = ex + } + + override fun setMessage(message: String) { + msg = { message } + } + + override fun setMessage(message: MessageSupplier) { + msg = message + } + + // CRT logger does not support setting key-value pairs + override fun setKeyValuePair(key: String, value: Any) { } + + override fun emit() { + val message = requireNotNull(msg) { "no message provided to LogRecordBuilder" } + + val logMethod = when (level) { + LogLevel.Trace -> delegate::trace + LogLevel.Debug -> delegate::debug + LogLevel.Info -> delegate::info + LogLevel.Warning -> delegate::warn + LogLevel.Error -> delegate::error + } + + logMethod(cause, message) + } +} diff --git a/runtime/observability/logging-crt/native/src/aws/smithy/kotlin/runtime/telemetry/logging/crt/CrtLogger.kt b/runtime/observability/logging-crt/native/src/aws/smithy/kotlin/runtime/telemetry/logging/crt/CrtLogger.kt new file mode 100644 index 0000000000..1a88eb068b --- /dev/null +++ b/runtime/observability/logging-crt/native/src/aws/smithy/kotlin/runtime/telemetry/logging/crt/CrtLogger.kt @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.telemetry.logging.crt + +import aws.sdk.kotlin.crt.WithCrt +import aws.sdk.kotlin.crt.log +import aws.smithy.kotlin.runtime.telemetry.logging.LogLevel +import aws.smithy.kotlin.runtime.telemetry.logging.LogRecordBuilder +import aws.smithy.kotlin.runtime.telemetry.logging.Logger +import aws.smithy.kotlin.runtime.telemetry.logging.MessageSupplier +import aws.sdk.kotlin.crt.Config as CrtConfig +import aws.sdk.kotlin.crt.LogLevel as CrtLogLevel + +public class CrtLogger(public val name: String, public val config: CrtConfig) : + WithCrt(), + Logger { + override fun trace(t: Throwable?, msg: MessageSupplier): Unit = log(CrtLogLevel.Trace, msg()) + override fun debug(t: Throwable?, msg: MessageSupplier): Unit = log(CrtLogLevel.Debug, msg()) + override fun info(t: Throwable?, msg: MessageSupplier): Unit = log(CrtLogLevel.Info, msg()) + override fun warn(t: Throwable?, msg: MessageSupplier): Unit = log(CrtLogLevel.Warn, msg()) + override fun error(t: Throwable?, msg: MessageSupplier): Unit = log(CrtLogLevel.Error, msg()) + override fun isEnabledFor(level: LogLevel): Boolean = config.logLevel.ordinal >= level.ordinal + override fun atLevel(level: LogLevel): LogRecordBuilder = CrtLogRecordBuilder(this, level) +} diff --git a/runtime/observability/logging-crt/native/src/aws/smithy/kotlin/runtime/telemetry/logging/crt/CrtLoggerProvider.kt b/runtime/observability/logging-crt/native/src/aws/smithy/kotlin/runtime/telemetry/logging/crt/CrtLoggerProvider.kt new file mode 100644 index 0000000000..31259e9804 --- /dev/null +++ b/runtime/observability/logging-crt/native/src/aws/smithy/kotlin/runtime/telemetry/logging/crt/CrtLoggerProvider.kt @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.telemetry.logging.crt + +import aws.smithy.kotlin.runtime.telemetry.logging.* +import aws.sdk.kotlin.crt.Config as CrtConfig + +public class CrtLoggerProvider : LoggerProvider { + override fun getOrCreateLogger(name: String): Logger = CrtLogger(name, CrtConfig()) + public fun getOrCreateLogger(name: String, config: CrtConfig): Logger = CrtLogger(name, config) +} diff --git a/runtime/observability/logging-slf4j2/build.gradle.kts b/runtime/observability/logging-slf4j2/build.gradle.kts index 2fd9ad4c70..1f2bd3ad69 100644 --- a/runtime/observability/logging-slf4j2/build.gradle.kts +++ b/runtime/observability/logging-slf4j2/build.gradle.kts @@ -4,7 +4,7 @@ */ description = "Logging provider based on SLF4J" extra["displayName"] = "Smithy :: Kotlin :: Observability :: SLF4J binding" -extra["moduleName"] = "aws.smithy.kotlin.runtime.telemetry" +extra["moduleName"] = "aws.smithy.kotlin.runtime.telemetry.slf4j" kotlin { sourceSets { diff --git a/runtime/observability/telemetry-api/jvm/src/aws/smithy/kotlin/runtime/telemetry/context/TelemetryContextElementJVM.kt b/runtime/observability/telemetry-api/jvm/src/aws/smithy/kotlin/runtime/telemetry/context/TelemetryContextElementJVM.kt index d6680f0d69..e8548ad31c 100644 --- a/runtime/observability/telemetry-api/jvm/src/aws/smithy/kotlin/runtime/telemetry/context/TelemetryContextElementJVM.kt +++ b/runtime/observability/telemetry-api/jvm/src/aws/smithy/kotlin/runtime/telemetry/context/TelemetryContextElementJVM.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.ThreadContextElement import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.CoroutineContext +// FIXME Move to jvmAndNative when https://github.com/Kotlin/kotlinx.coroutines/issues/3326 is implemented @InternalApi public actual class TelemetryContextElement public actual constructor( public actual val context: Context, diff --git a/runtime/observability/telemetry-defaults/build.gradle.kts b/runtime/observability/telemetry-defaults/build.gradle.kts index e77655c91d..357b11d80e 100644 --- a/runtime/observability/telemetry-defaults/build.gradle.kts +++ b/runtime/observability/telemetry-defaults/build.gradle.kts @@ -24,6 +24,12 @@ kotlin { } } + nativeMain { + dependencies { + implementation(project(":runtime:observability:logging-crt")) + } + } + all { languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi") } diff --git a/runtime/observability/telemetry-defaults/native/src/aws/smithy/kotlin/runtime/telemetry/logging/DefaultLoggerProviderNative.kt b/runtime/observability/telemetry-defaults/native/src/aws/smithy/kotlin/runtime/telemetry/logging/DefaultLoggerProviderNative.kt index 1d8bec0441..506d10869d 100644 --- a/runtime/observability/telemetry-defaults/native/src/aws/smithy/kotlin/runtime/telemetry/logging/DefaultLoggerProviderNative.kt +++ b/runtime/observability/telemetry-defaults/native/src/aws/smithy/kotlin/runtime/telemetry/logging/DefaultLoggerProviderNative.kt @@ -5,4 +5,6 @@ package aws.smithy.kotlin.runtime.telemetry.logging -internal actual val DefaultLoggerProvider: LoggerProvider = TODO("Not yet implemented") +import aws.smithy.kotlin.runtime.telemetry.logging.crt.CrtLoggerProvider + +internal actual val DefaultLoggerProvider: LoggerProvider = CrtLoggerProvider() diff --git a/runtime/protocol/aws-event-stream/common/test/aws/smithy/kotlin/runtime/awsprotocol/eventstream/FrameDecoderTest.kt b/runtime/protocol/aws-event-stream/common/test/aws/smithy/kotlin/runtime/awsprotocol/eventstream/FrameDecoderTest.kt index dd118def5a..4e35cb58dc 100644 --- a/runtime/protocol/aws-event-stream/common/test/aws/smithy/kotlin/runtime/awsprotocol/eventstream/FrameDecoderTest.kt +++ b/runtime/protocol/aws-event-stream/common/test/aws/smithy/kotlin/runtime/awsprotocol/eventstream/FrameDecoderTest.kt @@ -14,7 +14,6 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class FrameDecoderTest { - @Test fun testFrameStreamSingleMessage() = runTest { val encoded = validMessageWithAllHeaders() diff --git a/runtime/protocol/aws-event-stream/common/test/aws/smithy/kotlin/runtime/awsprotocol/eventstream/HeaderValueTest.kt b/runtime/protocol/aws-event-stream/common/test/aws/smithy/kotlin/runtime/awsprotocol/eventstream/HeaderValueTest.kt index 12895d367f..16602572ea 100644 --- a/runtime/protocol/aws-event-stream/common/test/aws/smithy/kotlin/runtime/awsprotocol/eventstream/HeaderValueTest.kt +++ b/runtime/protocol/aws-event-stream/common/test/aws/smithy/kotlin/runtime/awsprotocol/eventstream/HeaderValueTest.kt @@ -14,6 +14,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFails class HeaderValueTest { + @Test fun testExpectAs() { assertEquals(true, HeaderValue.Bool(true).expectBool()) diff --git a/runtime/protocol/aws-json-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/json/AwsJsonProtocolTest.kt b/runtime/protocol/aws-json-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/json/AwsJsonProtocolTest.kt index 172d831283..344ce79e0d 100644 --- a/runtime/protocol/aws-json-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/json/AwsJsonProtocolTest.kt +++ b/runtime/protocol/aws-json-protocols/common/test/aws/smithy/kotlin/runtime/awsprotocol/json/AwsJsonProtocolTest.kt @@ -19,7 +19,6 @@ import kotlin.test.Test import kotlin.test.assertEquals class AwsJsonProtocolTest { - @Test fun testSetJsonProtocolHeaders() = runTest { val op = SdkHttpOperation.build { diff --git a/runtime/protocol/aws-protocol-core/common/test/ClockSkewInterceptorTest.kt b/runtime/protocol/aws-protocol-core/common/test/ClockSkewInterceptorTest.kt index 930b8ee646..71afea0700 100644 --- a/runtime/protocol/aws-protocol-core/common/test/ClockSkewInterceptorTest.kt +++ b/runtime/protocol/aws-protocol-core/common/test/ClockSkewInterceptorTest.kt @@ -33,8 +33,8 @@ private val NOT_SKEWED_RESPONSE_CODE_DESCRIPTION = "RequestThrottled" class ClockSkewInterceptorTest { @Test fun testNotSkewed() { - val clientTime = Instant.fromRfc5322("Wed, 6 Oct 2023 16:20:50 -0400") - val serverTime = Instant.fromRfc5322("Wed, 6 Oct 2023 16:20:50 -0400") + val clientTime = Instant.fromRfc5322("Fri, 6 Oct 2023 16:20:50 -0400") + val serverTime = Instant.fromRfc5322("Fri, 6 Oct 2023 16:20:50 -0400") assertEquals(clientTime, serverTime) assertFalse(clientTime.isSkewed(serverTime, NOT_SKEWED_RESPONSE_CODE_DESCRIPTION)) } @@ -42,24 +42,24 @@ class ClockSkewInterceptorTest { @Test fun testSkewedByResponseCode() { // clocks are exactly the same, but service returned skew error - val clientTime = Instant.fromRfc5322("Wed, 6 Oct 2023 16:20:50 -0400") - val serverTime = Instant.fromRfc5322("Wed, 6 Oct 2023 16:20:50 -0400") + val clientTime = Instant.fromRfc5322("Fri, 6 Oct 2023 16:20:50 -0400") + val serverTime = Instant.fromRfc5322("Fri, 6 Oct 2023 16:20:50 -0400") assertTrue(clientTime.isSkewed(serverTime, SKEWED_RESPONSE_CODE_DESCRIPTION)) assertEquals(0.days, clientTime.until(serverTime)) } @Test fun testSkewedByTime() { - val clientTime = Instant.fromRfc5322("Wed, 6 Oct 2023 16:20:50 -0400") - val serverTime = Instant.fromRfc5322("Wed, 7 Oct 2023 16:20:50 -0400") + val clientTime = Instant.fromRfc5322("Fri, 6 Oct 2023 16:20:50 -0400") + val serverTime = Instant.fromRfc5322("Sat, 7 Oct 2023 16:20:50 -0400") assertTrue(clientTime.isSkewed(serverTime, POSSIBLE_SKEWED_RESPONSE_CODE_DESCRIPTION)) assertEquals(1.days, clientTime.until(serverTime)) } @Test fun testNegativeSkewedByTime() { - val clientTime = Instant.fromRfc5322("Wed, 7 Oct 2023 16:20:50 -0400") - val serverTime = Instant.fromRfc5322("Wed, 6 Oct 2023 16:20:50 -0400") + val clientTime = Instant.fromRfc5322("Sat, 7 Oct 2023 16:20:50 -0400") + val serverTime = Instant.fromRfc5322("Fri, 6 Oct 2023 16:20:50 -0400") assertTrue(clientTime.isSkewed(serverTime, POSSIBLE_SKEWED_RESPONSE_CODE_DESCRIPTION)) assertEquals(-1.days, clientTime.until(serverTime)) } @@ -68,8 +68,8 @@ class ClockSkewInterceptorTest { fun testSkewThreshold() { val minute = 20 var clientTime = - Instant.fromRfc5322("Wed, 6 Oct 2023 16:${minute - CLOCK_SKEW_THRESHOLD.inWholeMinutes}:50 -0400") - val serverTime = Instant.fromRfc5322("Wed, 6 Oct 2023 16:$minute:50 -0400") + Instant.fromRfc5322("Fri, 6 Oct 2023 16:${minute - CLOCK_SKEW_THRESHOLD.inWholeMinutes}:50 -0400") + val serverTime = Instant.fromRfc5322("Fri, 6 Oct 2023 16:$minute:50 -0400") assertTrue(clientTime.isSkewed(serverTime, POSSIBLE_SKEWED_RESPONSE_CODE_DESCRIPTION)) assertEquals(CLOCK_SKEW_THRESHOLD, clientTime.until(serverTime)) @@ -128,7 +128,7 @@ class ClockSkewInterceptorTest { @Test fun testClockSkewApplied() = runTest { testRoundTrip( - serverTimeString = "Wed, 14 Sep 2023 16:20:50 -0400", // Big skew + serverTimeString = "Thu, 14 Sep 2023 16:20:50 -0400", // Big skew clientTimeString = "20231006T131604Z", httpStatusCode = HttpStatusCode.Forbidden, expectException = false, diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/build.gradle.kts b/runtime/protocol/http-client-engines/http-client-engine-crt/build.gradle.kts index d2f411578f..b3a3f5507a 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-crt/build.gradle.kts +++ b/runtime/protocol/http-client-engines/http-client-engine-crt/build.gradle.kts @@ -11,18 +11,18 @@ apply(plugin = "org.jetbrains.kotlinx.atomicfu") kotlin { sourceSets { - jvmMain { + jvmAndNativeMain { dependencies { api(project(":runtime:runtime-core")) api(project(":runtime:protocol:http-client")) implementation(project(":runtime:crt-util")) implementation(project(":runtime:observability:telemetry-api")) - implementation(libs.kotlinx.coroutines.core) + api(libs.crt.kotlin) } } - jvmTest { + jvmAndNativeTest { dependencies { implementation(project(":runtime:testing")) implementation(project(":runtime:protocol:http-test")) diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/ConnectionManager.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/ConnectionManager.kt similarity index 92% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/ConnectionManager.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/ConnectionManager.kt index 79d7bc46a9..6009d41576 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/ConnectionManager.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/ConnectionManager.kt @@ -5,11 +5,11 @@ package aws.smithy.kotlin.runtime.http.engine.crt import aws.sdk.kotlin.crt.http.* +import aws.sdk.kotlin.crt.io.ClientBootstrap import aws.sdk.kotlin.crt.io.SocketOptions import aws.sdk.kotlin.crt.io.TlsContext import aws.sdk.kotlin.crt.io.TlsContextOptionsBuilder import aws.sdk.kotlin.crt.io.Uri -import aws.smithy.kotlin.runtime.crt.SdkDefaultIO import aws.smithy.kotlin.runtime.http.HttpErrorCode import aws.smithy.kotlin.runtime.http.HttpException import aws.smithy.kotlin.runtime.http.engine.ProxyConfig @@ -43,8 +43,11 @@ internal class ConnectionManager( .build() .let(::CrtTlsContext) + private val manageClientBootstrap = config.clientBootstrap == null + private val clientBootstrap = config.clientBootstrap ?: ClientBootstrap() + private val options = HttpClientConnectionManagerOptionsBuilder().apply { - clientBootstrap = config.clientBootstrap ?: SdkDefaultIO.ClientBootstrap + clientBootstrap = this@ConnectionManager.clientBootstrap tlsContext = crtTlsContext manualWindowManagement = true socketOptions = SocketOptions( @@ -59,7 +62,7 @@ internal class ConnectionManager( private val connManagers = mutableMapOf() private val mutex = Mutex() - public suspend fun acquire(request: HttpRequest): HttpClientConnection { + suspend fun acquire(request: HttpRequest): HttpClientConnection { val proxyConfig = config.proxySelector.select(request.url) val manager = getManagerForUri(request.uri, proxyConfig) @@ -92,6 +95,7 @@ internal class ConnectionManager( throw httpEx } } + private suspend fun getManagerForUri(uri: Uri, proxyConfig: ProxyConfig): HttpClientConnectionManager = mutex.withLock { connManagers.getOrPut(uri.authority) { val connOpts = options.apply { @@ -113,9 +117,12 @@ internal class ConnectionManager( HttpClientConnectionManager(connOpts) } } + override fun close() { connManagers.forEach { entry -> entry.value.close() } crtTlsContext.close() + + if (manageClientBootstrap) clientBootstrap.close() } private inner class LeasedConnection(private val delegate: HttpClientConnection) : HttpClientConnection by delegate { @@ -131,7 +138,7 @@ internal class ConnectionManager( } private fun toCrtTlsVersion(sdkTlsVersion: SdkTlsVersion?): CrtTlsVersion = when (sdkTlsVersion) { - null -> aws.sdk.kotlin.crt.io.TlsVersion.SYS_DEFAULT + null -> CrtTlsVersion.SYS_DEFAULT TlsVersion.TLS_1_0 -> CrtTlsVersion.TLSv1 TlsVersion.TLS_1_1 -> CrtTlsVersion.TLS_V1_1 TlsVersion.TLS_1_2 -> CrtTlsVersion.TLS_V1_2 diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngine.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngine.kt similarity index 98% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngine.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngine.kt index 40a38a9a90..de0e3b5457 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngine.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngine.kt @@ -15,9 +15,10 @@ import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers import aws.smithy.kotlin.runtime.operation.ExecutionContext import aws.smithy.kotlin.runtime.telemetry.logging.logger import aws.smithy.kotlin.runtime.time.Instant -import kotlinx.coroutines.* +import kotlinx.coroutines.job import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext internal const val DEFAULT_WINDOW_SIZE_BYTES: Int = 16 * 1024 internal const val CHUNK_BUFFER_SIZE: Long = 64 * 1024 diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig.kt similarity index 100% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/CrtHttpEngineConfig.kt diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/RequestUtil.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/RequestUtil.kt similarity index 100% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/RequestUtil.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/RequestUtil.kt diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandler.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandler.kt similarity index 100% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/src/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandler.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/src/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandler.kt diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/AsyncStressTest.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/AsyncStressTest.kt similarity index 66% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/AsyncStressTest.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/AsyncStressTest.kt index 7ad57fa1a0..a4b9bc51be 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/AsyncStressTest.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/AsyncStressTest.kt @@ -11,9 +11,9 @@ import aws.smithy.kotlin.runtime.http.complete import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder import aws.smithy.kotlin.runtime.http.request.url import aws.smithy.kotlin.runtime.httptest.TestWithLocalServer +import aws.smithy.kotlin.runtime.io.use import aws.smithy.kotlin.runtime.net.Host import aws.smithy.kotlin.runtime.net.Scheme -import aws.smithy.kotlin.runtime.testing.IgnoreWindows import io.ktor.server.cio.* import io.ktor.server.engine.* import io.ktor.server.response.* @@ -26,7 +26,6 @@ import kotlin.test.Test import kotlin.time.Duration.Companion.seconds class AsyncStressTest : TestWithLocalServer() { - override val server = embeddedServer(CIO, serverPort) { routing { get("/largeResponse") { @@ -36,39 +35,40 @@ class AsyncStressTest : TestWithLocalServer() { call.respondText(text.repeat(respSize / text.length)) } } - } + }.start() - @IgnoreWindows("FIXME - times out after upgrade to kotlinx.coroutines 1.6.0") @Test fun testStreamNotConsumed() = runBlocking { // test that filling the stream window and not consuming the body stream still cleans up resources // appropriately and allows requests to proceed (a stream that isn't consumed will be in a stuck state // if the window is full and never incremented again, this can lead to all connections being consumed // and the engine to no longer make further requests) - val client = SdkHttpClient(CrtHttpEngine()) - val request = HttpRequestBuilder().apply { - url { - scheme = Scheme.HTTP - method = HttpMethod.GET - host = Host.Domain(testHost) - port = serverPort - path.decoded = "/largeResponse" + CrtHttpEngine().use { engine -> + val client = SdkHttpClient(engine) + val request = HttpRequestBuilder().apply { + url { + scheme = Scheme.HTTP + method = HttpMethod.GET + host = Host.Domain(testHost) + port = serverPort + path.decoded = "/largeResponse" + } } - } - withTimeout(10.seconds) { - repeat(1_000) { - async { - try { - val call = client.call(request) - yield() - call.complete() - } catch (ex: Exception) { - println("exception on $it: $ex") - throw ex + withTimeout(10.seconds) { + repeat(1_000) { + async { + try { + val call = client.call(request) + yield() + call.complete() + } catch (ex: Exception) { + println("exception on $it: $ex") + throw ex + } } + yield() } - yield() } } } diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/RequestConversionTest.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/RequestConversionTest.kt similarity index 100% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/RequestConversionTest.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/RequestConversionTest.kt diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/RequestUtilTest.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/RequestUtilTest.kt similarity index 100% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/RequestUtilTest.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/RequestUtilTest.kt diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandlerTest.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandlerTest.kt similarity index 96% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandlerTest.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandlerTest.kt index 9bbc35c4cd..6c09f0dd02 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandlerTest.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandlerTest.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.http.engine.crt +import aws.sdk.kotlin.crt.CRT import aws.sdk.kotlin.crt.http.* import aws.sdk.kotlin.crt.io.byteArrayBuffer import aws.smithy.kotlin.runtime.http.HttpBody @@ -21,15 +22,17 @@ import kotlinx.coroutines.yield import kotlin.test.* class SdkStreamResponseHandlerTest { - private class MockHttpStream(override val responseStatusCode: Int) : HttpStream { var closed: Boolean = false override fun activate() {} + override suspend fun writeChunk(chunkData: ByteArray, isFinalChunk: Boolean) { + TODO("Not yet implemented") + } + override fun close() { closed = true } override fun incrementWindow(size: Int) {} - override fun writeChunk(chunkData: ByteArray, isFinalChunk: Boolean) {} } private class MockHttpClientConnection : HttpClientConnection { @@ -152,6 +155,7 @@ class SdkStreamResponseHandlerTest { @Test fun testStreamError() = runTest { + CRT.initRuntime() // CRT needs to be initialized for human-readable error codes val handler = SdkStreamResponseHandler(mockConn, coroutineContext) val stream = MockHttpStream(200) val data = "foo bar" diff --git a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/SendChunkedBodyTest.kt b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/SendChunkedBodyTest.kt similarity index 92% rename from runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/SendChunkedBodyTest.kt rename to runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/SendChunkedBodyTest.kt index a7637b6753..88366c41f0 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-crt/jvm/test/aws/smithy/kotlin/runtime/http/engine/crt/SendChunkedBodyTest.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-crt/jvmAndNative/test/aws/smithy/kotlin/runtime/http/engine/crt/SendChunkedBodyTest.kt @@ -22,7 +22,7 @@ class SendChunkedBodyTest { closed = true } override fun incrementWindow(size: Int) {} - override fun writeChunk(chunkData: ByteArray, isFinalChunk: Boolean) { + override suspend fun writeChunk(chunkData: ByteArray, isFinalChunk: Boolean) { numChunksWritten += 1 } } @@ -33,7 +33,7 @@ class SendChunkedBodyTest { val chunkedBytes = """ 100;chunk-signature=${"0".repeat(64)}\r\n${"0".repeat(256)}\r\n\r\n - """.trimIndent().toByteArray() + """.trimIndent().encodeToByteArray() val source = chunkedBytes.source() @@ -52,7 +52,7 @@ class SendChunkedBodyTest { val chunkedBytes = """ ${chunkSize.toString(16)};chunk-signature=${"0".repeat(64)}\r\n${"0".repeat(chunkSize)}\r\n\r\n - """.trimIndent().toByteArray() + """.trimIndent().encodeToByteArray() val source = chunkedBytes.source() @@ -71,7 +71,7 @@ class SendChunkedBodyTest { val chunkedBytes = """ 100;chunk-signature=${"0".repeat(64)}\r\n${"0".repeat(256)}\r\n\r\n - """.trimIndent().toByteArray() + """.trimIndent().encodeToByteArray() val channel = SdkByteReadChannel(chunkedBytes) @@ -91,7 +91,7 @@ class SendChunkedBodyTest { val chunkedBytes = """ ${chunkSize.toString(16)};chunk-signature=${"0".repeat(64)}\r\n${"0".repeat(chunkSize)}\r\n\r\n - """.trimIndent().toByteArray() + """.trimIndent().encodeToByteArray() val channel = SdkByteReadChannel(chunkedBytes) diff --git a/runtime/protocol/http-client-engines/http-client-engine-default/build.gradle.kts b/runtime/protocol/http-client-engines/http-client-engine-default/build.gradle.kts index d2381c39f0..321be4f532 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-default/build.gradle.kts +++ b/runtime/protocol/http-client-engines/http-client-engine-default/build.gradle.kts @@ -16,19 +16,28 @@ kotlin { implementation(project(":runtime:runtime-core")) } } + jvmMain { dependencies { // okhttp works on both JVM and Android implementation(project(":runtime:protocol:http-client-engines:http-client-engine-okhttp")) } } + jvmTest { dependencies { implementation(project(":runtime:protocol:http-client-engines:http-client-engine-crt")) } } + all { languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi") } + + nativeMain { + dependencies { + implementation(project(":runtime:protocol:http-client-engines:http-client-engine-crt")) + } + } } } diff --git a/runtime/protocol/http-client-engines/http-client-engine-default/native/src/aws/smithy/kotlin/runtime/http/engine/DefaultHttpEngineNative.kt b/runtime/protocol/http-client-engines/http-client-engine-default/native/src/aws/smithy/kotlin/runtime/http/engine/DefaultHttpEngineNative.kt index d9d060337f..7f74397245 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-default/native/src/aws/smithy/kotlin/runtime/http/engine/DefaultHttpEngineNative.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-default/native/src/aws/smithy/kotlin/runtime/http/engine/DefaultHttpEngineNative.kt @@ -5,5 +5,11 @@ package aws.smithy.kotlin.runtime.http.engine -internal actual fun newDefaultHttpEngine(block: (HttpClientEngineConfig.Builder.() -> Unit)?): CloseableHttpClientEngine = - TODO("Not yet implemented") +import aws.smithy.kotlin.runtime.http.engine.crt.CrtHttpEngine + +internal actual fun newDefaultHttpEngine( + block: (HttpClientEngineConfig.Builder.() -> Unit)?, +): CloseableHttpClientEngine = when (block) { + null -> CrtHttpEngine() + else -> CrtHttpEngine(block) +} diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/build.gradle.kts b/runtime/protocol/http-client-engines/http-client-engine-okhttp/build.gradle.kts index 3e5d139381..0e12749c09 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/build.gradle.kts +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/build.gradle.kts @@ -9,7 +9,7 @@ extra["moduleName"] = "aws.smithy.kotlin.runtime.http.engine.okhttp" kotlin { sourceSets { - commonMain { + jvmMain { dependencies { implementation(project(":runtime:runtime-core")) api(project(":runtime:protocol:http-client")) diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/build.gradle.kts b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/build.gradle.kts index 26145b4065..f6b3f12668 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/build.gradle.kts +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/build.gradle.kts @@ -9,7 +9,7 @@ extra["moduleName"] = "aws.smithy.kotlin.runtime.http.engine.okhttp4" kotlin { sourceSets { - commonMain { + jvmMain { dependencies { api(project(":runtime:protocol:http-client")) implementation(project(":runtime:protocol:http-client-engines:http-client-engine-okhttp")) diff --git a/runtime/protocol/http-client-engines/test-suite/build.gradle.kts b/runtime/protocol/http-client-engines/test-suite/build.gradle.kts index d2487c2722..e9c3e6555d 100644 --- a/runtime/protocol/http-client-engines/test-suite/build.gradle.kts +++ b/runtime/protocol/http-client-engines/test-suite/build.gradle.kts @@ -39,6 +39,7 @@ kotlin { jvmAndNativeMain { dependencies { + implementation(libs.ktor.server.core) implementation(libs.kotlinx.coroutines.test) } } diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestJVM.kt b/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestJVM.kt index fa091c1f03..6f0d9182d1 100644 --- a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestJVM.kt +++ b/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestJVM.kt @@ -11,6 +11,7 @@ import aws.smithy.kotlin.runtime.http.engine.okhttp4.OkHttp4Engine import aws.smithy.kotlin.runtime.net.url.Url internal actual fun engineFactories(): List = + // FIXME Move DefaultHttpEngine and CrtHttpEngine to `jvmAndNative` listOf( TestEngineFactory("DefaultHttpEngine", ::DefaultHttpEngine), TestEngineFactory("CrtHttpEngine") { CrtHttpEngine(it) }, diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/TestServers.kt b/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/TestServers.kt index 1bc9d6e9ea..24c001d5b2 100644 --- a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/TestServers.kt +++ b/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/TestServers.kt @@ -12,7 +12,6 @@ import aws.smithy.kotlin.runtime.http.test.suite.tlsTests import aws.smithy.kotlin.runtime.http.test.suite.uploadTests import io.ktor.server.application.* import io.ktor.server.engine.* -import io.ktor.server.jetty.* import io.ktor.server.jetty.jakarta.Jetty import io.ktor.server.jetty.jakarta.JettyApplicationEngineBase import redirectTests diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Concurrency.kt b/runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Concurrency.kt similarity index 88% rename from runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Concurrency.kt rename to runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Concurrency.kt index fad3e7fec9..b3b482fa20 100644 --- a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Concurrency.kt +++ b/runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Concurrency.kt @@ -8,6 +8,7 @@ package aws.smithy.kotlin.runtime.http.test.suite import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* +import io.ktor.utils.io.writeFully import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.milliseconds @@ -24,10 +25,10 @@ internal fun Application.concurrentTests() { route("slow") { get { val chunk = ByteArray(256) { it.toByte() } - call.respondOutputStream { + call.respondBytesWriter { repeat(10) { delay(200.milliseconds) - write(chunk) + writeFully(chunk) } } } diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Connections.kt b/runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Connections.kt similarity index 100% rename from runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Connections.kt rename to runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Connections.kt diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Downloads.kt b/runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Downloads.kt similarity index 84% rename from runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Downloads.kt rename to runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Downloads.kt index dd495edf24..7cd8ba93e6 100644 --- a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Downloads.kt +++ b/runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Downloads.kt @@ -6,6 +6,9 @@ package aws.smithy.kotlin.runtime.http.test.suite import aws.smithy.kotlin.runtime.hashing.sha256 +import aws.smithy.kotlin.runtime.io.GzipSdkSource +import aws.smithy.kotlin.runtime.io.readToByteArray +import aws.smithy.kotlin.runtime.io.source import aws.smithy.kotlin.runtime.text.encoding.encodeToHex import io.ktor.http.* import io.ktor.server.application.* @@ -13,8 +16,6 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.utils.io.* import kotlinx.coroutines.delay -import java.io.ByteArrayOutputStream -import java.util.zip.GZIPOutputStream import kotlin.random.Random internal const val DOWNLOAD_SIZE = 16L * 1024 * 1024 // 16MB @@ -55,15 +56,7 @@ internal fun Application.downloadTests() { get("/gzipped") { val uncompressed = ByteArray(1024) { it.toByte() } - val compressed = ByteArrayOutputStream().use { baStream -> - GZIPOutputStream(baStream).use { gzStream -> - gzStream.write(uncompressed) - gzStream.flush() - } - baStream.flush() - baStream.toByteArray() - } - + val compressed = GzipSdkSource(uncompressed.source()).readToByteArray() call.response.header("Content-Encoding", "gzip") call.respondBytes(compressed) } diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Header.kt b/runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Header.kt similarity index 100% rename from runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Header.kt rename to runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Header.kt diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Redirects.kt b/runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Redirects.kt similarity index 100% rename from runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Redirects.kt rename to runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Redirects.kt diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Tls.kt b/runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Tls.kt similarity index 100% rename from runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Tls.kt rename to runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Tls.kt diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Uploads.kt b/runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Uploads.kt similarity index 100% rename from runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Uploads.kt rename to runtime/protocol/http-client-engines/test-suite/jvmAndNative/src/aws/smithy/kotlin/runtime/http/test/suite/Uploads.kt diff --git a/runtime/protocol/http-client-engines/test-suite/native/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestNative.kt b/runtime/protocol/http-client-engines/test-suite/native/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestNative.kt index bfb3ff18f4..05f82f63d0 100644 --- a/runtime/protocol/http-client-engines/test-suite/native/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestNative.kt +++ b/runtime/protocol/http-client-engines/test-suite/native/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestNative.kt @@ -7,7 +7,7 @@ package aws.smithy.kotlin.runtime.http.test.util import aws.smithy.kotlin.runtime.net.url.Url -// FIXME add engines to test +// FIXME Move CRT and Default engine tests to `jvmAndNative` internal actual fun engineFactories(): List = listOf() 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 55641129d9..11163c4a61 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 @@ -8,6 +8,7 @@ package aws.smithy.kotlin.runtime.http.interceptors 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 @@ -22,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()) diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/HttpInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/HttpInterceptorTest.kt index b03f099ccb..5d36840bcf 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/HttpInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/operation/HttpInterceptorTest.kt @@ -259,7 +259,9 @@ class HttpInterceptorTest { op.roundTrip(client, Unit) } - val cause = assertNotNull(ex.cause) + // FIXME Investigate why the exception `ex` and its cause are duplicated on JVM. + // On JVM, `ex.cause` has the suppressed exceptions. On Native, `ex` has the suppressed exceptions and it has no cause. + val cause = assertNotNull(ex.cause ?: ex) assertEquals(1, cause.suppressedExceptions.size) assertIs(cause.suppressedExceptions.last()) } 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 74cbd525c2..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") -} diff --git a/runtime/protocol/http-test/build.gradle.kts b/runtime/protocol/http-test/build.gradle.kts index a2077a979c..e966832924 100644 --- a/runtime/protocol/http-test/build.gradle.kts +++ b/runtime/protocol/http-test/build.gradle.kts @@ -28,6 +28,14 @@ kotlin { } } + jvmAndNativeMain { + dependencies { + api(libs.ktor.server.cio) + implementation(libs.ktor.client.core) + implementation(libs.ktor.server.core) + } + } + all { languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi") } diff --git a/runtime/protocol/http-test/jvm/src/aws/smithy/kotlin/runtime/httptest/TestWithLocalServer.kt b/runtime/protocol/http-test/jvmAndNative/src/aws/smithy/kotlin/runtime/httptest/TestWithLocalServer.kt similarity index 63% rename from runtime/protocol/http-test/jvm/src/aws/smithy/kotlin/runtime/httptest/TestWithLocalServer.kt rename to runtime/protocol/http-test/jvmAndNative/src/aws/smithy/kotlin/runtime/httptest/TestWithLocalServer.kt index 3ffc1a868c..ed76448c10 100644 --- a/runtime/protocol/http-test/jvm/src/aws/smithy/kotlin/runtime/httptest/TestWithLocalServer.kt +++ b/runtime/protocol/http-test/jvmAndNative/src/aws/smithy/kotlin/runtime/httptest/TestWithLocalServer.kt @@ -5,11 +5,14 @@ package aws.smithy.kotlin.runtime.httptest +import io.ktor.network.selector.* +import io.ktor.network.sockets.* +import io.ktor.server.application.* import io.ktor.server.engine.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout -import java.net.* -import java.util.concurrent.* import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.time.Duration.Companion.seconds @@ -19,13 +22,27 @@ import kotlin.time.Duration.Companion.seconds * mocking an HTTP client engine is difficult. */ public abstract class TestWithLocalServer { - protected val serverPort: Int = ServerSocket(0).use { it.localPort } + protected val serverPort: Int = runBlocking { + SelectorManager(this.coroutineContext).use { + aSocket(it) + .tcp() + .bind() + .use { (it.localAddress as InetSocketAddress).port } + } + } + protected val testHost: String = "localhost" public abstract val server: EmbeddedServer<*, *> @BeforeTest public fun startServer(): Unit = runBlocking { + val serverStarted = CompletableDeferred() + + server.monitor.subscribe(ApplicationStarted) { + serverStarted.complete(Unit) + } + withTimeout(5.seconds) { var attempt = 0 @@ -33,32 +50,20 @@ public abstract class TestWithLocalServer { attempt++ try { server.start() - println("test server listening on: $testHost:$serverPort") break } catch (cause: Throwable) { if (attempt >= 10) throw cause - Thread.sleep(250L * attempt) + delay(250L * attempt) } } while (true) - ensureServerRunning() + serverStarted.await() } } @AfterTest public fun stopServer() { - server.stop(0, 0, TimeUnit.SECONDS) + server.stop(0, 0) println("test server stopped") } - - private fun ensureServerRunning() { - do { - try { - Socket("localhost", serverPort).close() - break - } catch (_: Throwable) { - Thread.sleep(100) - } - } while (true) - } } diff --git a/runtime/protocol/http/common/test/aws/smithy/kotlin/runtime/http/HeadersTest.kt b/runtime/protocol/http/common/test/aws/smithy/kotlin/runtime/http/HeadersTest.kt index 12287dcc8d..7bd34fe5ae 100644 --- a/runtime/protocol/http/common/test/aws/smithy/kotlin/runtime/http/HeadersTest.kt +++ b/runtime/protocol/http/common/test/aws/smithy/kotlin/runtime/http/HeadersTest.kt @@ -10,7 +10,6 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class HeadersTest { - @Test fun itBuilds() { val actual = Headers { diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index fcc7f6727a..474ecffd2e 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -339,6 +339,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 @@ -375,6 +379,7 @@ public final class aws/smithy/kotlin/runtime/config/EnvironmentSettingKt { public final class aws/smithy/kotlin/runtime/content/BigDecimal : java/lang/Number, java/lang/Comparable { public fun (Laws/smithy/kotlin/runtime/content/BigInteger;I)V public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/math/BigDecimal;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun byteValue ()B public fun compareTo (Laws/smithy/kotlin/runtime/content/BigDecimal;)I public synthetic fun compareTo (Ljava/lang/Object;)I @@ -383,9 +388,11 @@ public final class aws/smithy/kotlin/runtime/content/BigDecimal : java/lang/Numb public final fun floatValue ()F public final fun getExponent ()I public final fun getMantissa ()Laws/smithy/kotlin/runtime/content/BigInteger; - public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I public final fun intValue ()I public final fun longValue ()J + public final fun minus (Laws/smithy/kotlin/runtime/content/BigDecimal;)Laws/smithy/kotlin/runtime/content/BigDecimal; + public final fun plus (Laws/smithy/kotlin/runtime/content/BigDecimal;)Laws/smithy/kotlin/runtime/content/BigDecimal; public final fun shortValue ()S public fun toByte ()B public fun toDouble ()D @@ -394,6 +401,7 @@ public final class aws/smithy/kotlin/runtime/content/BigDecimal : java/lang/Numb public fun toLong ()J public final fun toPlainString ()Ljava/lang/String; public fun toShort ()S + public fun toString ()Ljava/lang/String; } public final class aws/smithy/kotlin/runtime/content/BigInteger : java/lang/Number, java/lang/Comparable { @@ -405,7 +413,6 @@ public final class aws/smithy/kotlin/runtime/content/BigInteger : java/lang/Numb public final fun doubleValue ()D public fun equals (Ljava/lang/Object;)Z public final fun floatValue ()F - public final fun getValue ()Ljava/lang/String; public fun hashCode ()I public final fun intValue ()I public final fun longValue ()J @@ -420,6 +427,8 @@ public final class aws/smithy/kotlin/runtime/content/BigInteger : java/lang/Numb public fun toLong ()J public fun toShort ()S public fun toString ()Ljava/lang/String; + public final fun toString (I)Ljava/lang/String; + public static synthetic fun toString$default (Laws/smithy/kotlin/runtime/content/BigInteger;IILjava/lang/Object;)Ljava/lang/String; } public abstract class aws/smithy/kotlin/runtime/content/ByteStream { @@ -469,6 +478,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; @@ -799,6 +810,12 @@ public final class aws/smithy/kotlin/runtime/io/ClosedWriteChannelException : ja public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V } +public class aws/smithy/kotlin/runtime/io/EOFException : java/io/EOFException { + public fun ()V + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V +} + public final class aws/smithy/kotlin/runtime/io/GzipByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel { public fun (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;)V public fun cancel (Ljava/lang/Throwable;)Z @@ -844,6 +861,10 @@ public final class aws/smithy/kotlin/runtime/io/HashingSource : aws/smithy/kotli } public final class aws/smithy/kotlin/runtime/io/JavaIOKt { + public static final fun inputStream (Laws/smithy/kotlin/runtime/io/SdkBuffer;)Ljava/io/InputStream; + public static final fun isOpen (Laws/smithy/kotlin/runtime/io/SdkBuffer;)Z + public static final fun outputStream (Laws/smithy/kotlin/runtime/io/SdkBuffer;)Ljava/io/OutputStream; + public static final fun read (Laws/smithy/kotlin/runtime/io/SdkBuffer;Ljava/nio/ByteBuffer;)I public static final fun sink (Ljava/io/File;)Laws/smithy/kotlin/runtime/io/SdkSink; public static final fun sink (Ljava/io/OutputStream;)Laws/smithy/kotlin/runtime/io/SdkSink; public static final fun sink (Ljava/nio/file/Path;)Laws/smithy/kotlin/runtime/io/SdkSink; @@ -854,6 +875,7 @@ public final class aws/smithy/kotlin/runtime/io/JavaIOKt { public static final fun source (Ljava/nio/file/Path;Lkotlin/ranges/LongRange;)Laws/smithy/kotlin/runtime/io/SdkSource; public static synthetic fun source$default (Ljava/io/File;JJILjava/lang/Object;)Laws/smithy/kotlin/runtime/io/SdkSource; public static synthetic fun source$default (Ljava/nio/file/Path;JJILjava/lang/Object;)Laws/smithy/kotlin/runtime/io/SdkSource; + public static final fun write (Laws/smithy/kotlin/runtime/io/SdkBuffer;Ljava/nio/ByteBuffer;)I } public final class aws/smithy/kotlin/runtime/io/SdkBuffer : aws/smithy/kotlin/runtime/io/SdkBufferedSink, aws/smithy/kotlin/runtime/io/SdkBufferedSource { @@ -866,12 +888,8 @@ public final class aws/smithy/kotlin/runtime/io/SdkBuffer : aws/smithy/kotlin/ru public fun getBuffer ()Laws/smithy/kotlin/runtime/io/SdkBuffer; public final fun getSize ()J public fun hashCode ()I - public fun inputStream ()Ljava/io/InputStream; - public fun isOpen ()Z - public fun outputStream ()Ljava/io/OutputStream; public fun peek ()Laws/smithy/kotlin/runtime/io/SdkBufferedSource; public fun read (Laws/smithy/kotlin/runtime/io/SdkBuffer;J)J - public fun read (Ljava/nio/ByteBuffer;)I public fun read ([BII)I public fun readAll (Laws/smithy/kotlin/runtime/io/SdkSink;)J public fun readByte ()B @@ -891,7 +909,6 @@ public final class aws/smithy/kotlin/runtime/io/SdkBuffer : aws/smithy/kotlin/ru public fun toString ()Ljava/lang/String; public fun write (Laws/smithy/kotlin/runtime/io/SdkBuffer;J)V public fun write (Laws/smithy/kotlin/runtime/io/SdkSource;J)V - public fun write (Ljava/nio/ByteBuffer;)I public fun write ([BII)V public fun writeAll (Laws/smithy/kotlin/runtime/io/SdkSource;)J public fun writeByte (B)V @@ -904,11 +921,9 @@ public final class aws/smithy/kotlin/runtime/io/SdkBuffer : aws/smithy/kotlin/ru public fun writeUtf8 (Ljava/lang/String;II)V } -public abstract interface class aws/smithy/kotlin/runtime/io/SdkBufferedSink : aws/smithy/kotlin/runtime/io/SdkSink, java/nio/channels/WritableByteChannel { +public abstract interface class aws/smithy/kotlin/runtime/io/SdkBufferedSink : aws/smithy/kotlin/runtime/io/SdkSink { public abstract fun emit ()V - public abstract fun flush ()V public abstract fun getBuffer ()Laws/smithy/kotlin/runtime/io/SdkBuffer; - public abstract fun outputStream ()Ljava/io/OutputStream; public abstract fun write (Laws/smithy/kotlin/runtime/io/SdkSource;J)V public abstract fun write ([BII)V public static synthetic fun write$default (Laws/smithy/kotlin/runtime/io/SdkBufferedSink;[BIIILjava/lang/Object;)V @@ -929,10 +944,9 @@ public final class aws/smithy/kotlin/runtime/io/SdkBufferedSink$DefaultImpls { public static synthetic fun writeUtf8$default (Laws/smithy/kotlin/runtime/io/SdkBufferedSink;Ljava/lang/String;IIILjava/lang/Object;)V } -public abstract interface class aws/smithy/kotlin/runtime/io/SdkBufferedSource : aws/smithy/kotlin/runtime/io/SdkSource, java/nio/channels/ReadableByteChannel { +public abstract interface class aws/smithy/kotlin/runtime/io/SdkBufferedSource : aws/smithy/kotlin/runtime/io/SdkSource { public abstract fun exhausted ()Z public abstract fun getBuffer ()Laws/smithy/kotlin/runtime/io/SdkBuffer; - public abstract fun inputStream ()Ljava/io/InputStream; public abstract fun peek ()Laws/smithy/kotlin/runtime/io/SdkBufferedSource; public abstract fun read ([BII)I public static synthetic fun read$default (Laws/smithy/kotlin/runtime/io/SdkBufferedSource;[BIIILjava/lang/Object;)I @@ -1068,16 +1082,13 @@ public abstract interface class aws/smithy/kotlin/runtime/io/SdkSource : java/io public abstract fun read (Laws/smithy/kotlin/runtime/io/SdkBuffer;J)J } -public final class aws/smithy/kotlin/runtime/io/SdkSourceJVMKt { +public final class aws/smithy/kotlin/runtime/io/SdkSourceKt { + public static final fun readFully (Laws/smithy/kotlin/runtime/io/SdkSource;Laws/smithy/kotlin/runtime/io/SdkBuffer;J)V public static final fun readToByteArray (Laws/smithy/kotlin/runtime/io/SdkSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun toSdkByteReadChannel (Laws/smithy/kotlin/runtime/io/SdkSource;Lkotlinx/coroutines/CoroutineScope;)Laws/smithy/kotlin/runtime/io/SdkByteReadChannel; public static synthetic fun toSdkByteReadChannel$default (Laws/smithy/kotlin/runtime/io/SdkSource;Lkotlinx/coroutines/CoroutineScope;ILjava/lang/Object;)Laws/smithy/kotlin/runtime/io/SdkByteReadChannel; } -public final class aws/smithy/kotlin/runtime/io/SdkSourceKt { - public static final fun readFully (Laws/smithy/kotlin/runtime/io/SdkSource;Laws/smithy/kotlin/runtime/io/SdkBuffer;J)V -} - public final class aws/smithy/kotlin/runtime/io/internal/ConvertKt { public static final fun toOkio (Laws/smithy/kotlin/runtime/io/SdkBuffer;)Lokio/Buffer; public static final fun toOkio (Laws/smithy/kotlin/runtime/io/SdkSink;)Lokio/Sink; @@ -2391,9 +2402,12 @@ public final class aws/smithy/kotlin/runtime/util/OperatingSystem { public final class aws/smithy/kotlin/runtime/util/OsFamily : java/lang/Enum { public static final field Android Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Ios Laws/smithy/kotlin/runtime/util/OsFamily; + public static final field IpadOs Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Linux Laws/smithy/kotlin/runtime/util/OsFamily; public static final field MacOs Laws/smithy/kotlin/runtime/util/OsFamily; + public static final field TvOs Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Unknown Laws/smithy/kotlin/runtime/util/OsFamily; + public static final field WatchOs Laws/smithy/kotlin/runtime/util/OsFamily; public static final field Windows Laws/smithy/kotlin/runtime/util/OsFamily; public static fun getEntries ()Lkotlin/enums/EnumEntries; public fun toString ()Ljava/lang/String; diff --git a/runtime/runtime-core/build.gradle.kts b/runtime/runtime-core/build.gradle.kts index bbaa9d60ef..2b6cc8ece4 100644 --- a/runtime/runtime-core/build.gradle.kts +++ b/runtime/runtime-core/build.gradle.kts @@ -3,6 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ +import aws.sdk.kotlin.gradle.kmp.NATIVE_ENABLED +import aws.sdk.kotlin.gradle.util.typedProp +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import org.jetbrains.kotlin.konan.target.HostManager +import java.nio.file.Files +import java.nio.file.Paths + plugins { alias(libs.plugins.kotlinx.serialization) } @@ -20,6 +27,14 @@ kotlin { implementation(libs.okio) // Coroutines' locking features are used in retry token bucket implementations api(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) + } + } + + nativeMain { + dependencies { + api(libs.crt.kotlin) + implementation(libs.kotlin.multiplatform.bignum) } } @@ -41,4 +56,77 @@ kotlin { languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi") } } + + if (NATIVE_ENABLED && !HostManager.hostIsMingw) { + targets.withType { + compilations["main"].cinterops { + val interopDir = "$projectDir/posix/src/posixInterop/cinterop" + create("environ") { + includeDirs(interopDir) + packageName("aws.smithy.platform.posix") + headers(listOf("$interopDir/environ.h")) + } + } + } + } + + if (NATIVE_ENABLED && HostManager.hostIsMingw) { + mingwX64 { + val mingwHome = findMingwHome() + val defPath = layout.buildDirectory.file("cinterop/winver.def") + + // Dynamically construct def file because of dynamic mingw paths + val defFileTask by tasks.registering { + outputs.file(defPath) + + val mingwLibs = Paths.get(mingwHome, "lib").toString().replace("\\", "\\\\") // Windows path shenanigans + + doLast { + Files.writeString( + defPath.get().asFile.toPath(), + """ + package = aws.smithy.kotlin.native.winver + headers = windows.h + compilerOpts = \ + -DUNICODE \ + -DWINVER=0x0601 \ + -D_WIN32_WINNT=0x0601 \ + -DWINAPI_FAMILY=3 \ + -DOEMRESOURCE \ + -Wno-incompatible-pointer-types \ + -Wno-deprecated-declarations + libraryPaths = $mingwLibs + staticLibraries = libversion.a + """.trimIndent(), + ) + } + } + compilations["main"].cinterops { + create("winver") { + val mingwIncludes = Paths.get(mingwHome, "include").toString() + includeDirs(mingwIncludes) + definitionFile.set(defPath) + + // Ensure that the def file is written first + tasks[interopProcessingTaskName].dependsOn(defFileTask) + } + } + + // TODO clean up + val compilerArgs = listOf( + "-Xverbose-phases=linker", // Enable verbose linking phase from the compiler + "-linker-option", + "-v", + ) + compilerOptions.freeCompilerArgs.addAll(compilerArgs) + } + } } + +private fun findMingwHome(): String = + System.getenv("MINGW_PREFIX")?.takeUnless { it.isBlank() } + ?: typedProp("mingw.prefix") + ?: throw IllegalStateException( + "Cannot determine MinGW prefix location. Please verify MinGW is installed correctly " + + "and that either the `MINGW_PREFIX` environment variable or the `mingw.prefix` Gradle property is set.", + ) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/compression/GzipTestUtils.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/compression/GzipTestUtils.kt new file mode 100644 index 0000000000..31b014acd0 --- /dev/null +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/compression/GzipTestUtils.kt @@ -0,0 +1,13 @@ +/* + * 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 + +/** + * Decompresses a [ByteArray] compressed using the gzip format + */ +@InternalApi +public expect fun decompressGzipBytes(bytes: ByteArray): ByteArray diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigDecimal.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigDecimal.kt index d9af8f85d6..408f4080f6 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigDecimal.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigDecimal.kt @@ -11,32 +11,94 @@ package aws.smithy.kotlin.runtime.content public expect class BigDecimal(value: String) : Number, Comparable { + /** * Create an instance of [BigDecimal] from a mantissa and exponent. - * @param mantissa a [BigInteger] representing the mantissa of this big decimal - * @param exponent an [Int] representing the exponent of this big decimal + * @param mantissa a [BigInteger] representing the [significant digits](https://en.wikipedia.org/wiki/Significand) + * of this decimal value + * @param exponent an [Int] representing the exponent of this decimal value */ public constructor(mantissa: BigInteger, exponent: Int) /** - * The mantissa of this decimal number + * The [significant digits](https://en.wikipedia.org/wiki/Significand) of this decimal value */ public val mantissa: BigInteger /** - * The exponent of this decimal number. - * If zero or positive, this represents the number of digits to the right of the decimal point. - * If negative, the mantissa is multiplied by ten to the power of the negation of the scale. + * The exponent of this decimal number. If zero or positive, this represents the number of digits to the right of + * the decimal point. If negative, the [mantissa] is multiplied by ten to the power of the negation of the scale. */ public val exponent: Int + /** + * Converts this value to a [Byte], which may involve rounding or truncation + */ override fun toByte(): Byte + + /** + * Converts this value to a [Double], which may involve rounding or truncation + */ override fun toDouble(): Double + + /** + * Converts this value to a [Float], which may involve rounding or truncation + */ override fun toFloat(): Float + + /** + * Converts this value to a [Short], which may involve rounding or truncation + */ override fun toShort(): Short + + /** + * Converts this value to an [Int], which may involve rounding or truncation + */ override fun toInt(): Int + + /** + * Converts this value to a [Long], which may involve rounding or truncation + */ override fun toLong(): Long + + /** + * Returns the decimal (i.e., radix-10) string representation of this value in long-form (i.e., _not_ scientific) + * notation + */ public fun toPlainString(): String + + /** + * Returns the decimal (i.e., radix-10) string representation of this value using scientific notation if an exponent + * is needed + */ + override fun toString(): String + + /** + * Returns a hash code for this value + */ + override fun hashCode(): Int + + /** + * Checks if this value is equal to the given object + * @param other The other value to compare against + */ override fun equals(other: Any?): Boolean + + /** + * Returns the sum of this value and the given value + * @param other The other value to add (i.e., the addend) + */ + public operator fun plus(other: BigDecimal): BigDecimal + + /** + * Returns the difference of this value and the given value + * @param other The value to subtract (i.e., the subtrahend) + */ + public operator fun minus(other: BigDecimal): BigDecimal + + /** + * Compare this value to the given value for in/equality + * @param other The value to compare against + */ public override operator fun compareTo(other: BigDecimal): Int } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigInteger.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigInteger.kt index f223bc36ca..798c653003 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigInteger.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/content/BigInteger.kt @@ -11,23 +11,86 @@ package aws.smithy.kotlin.runtime.content public expect class BigInteger(value: String) : Number, Comparable { + /** * Create an instance of [BigInteger] from a [ByteArray] * @param bytes ByteArray representing the large integer */ public constructor(bytes: ByteArray) + /** + * Converts this value to a [Byte], which may involve rounding or truncation + */ override fun toByte(): Byte + + /** + * Converts this value to a [Long], which may involve rounding or truncation + */ override fun toLong(): Long + + /** + * Converts this value to a [Short], which may involve rounding or truncation + */ override fun toShort(): Short + + /** + * Converts this value to an [Int], which may involve rounding or truncation + */ override fun toInt(): Int + + /** + * Converts this value to a [Float], which may involve rounding or truncation + */ override fun toFloat(): Float + + /** + * Converts this value to a [Double], which may involve rounding or truncation + */ override fun toDouble(): Double + + /** + * Returns the decimal (i.e., radix-10) string representation of this value + */ override fun toString(): String + + /** + * Returns a string representation of this value in the given radix + * @param radix The [numerical base](https://en.wikipedia.org/wiki/Radix) in which to represent the value + */ + public fun toString(radix: Int = 10): String + + /** + * Returns a hash code for this value + */ override fun hashCode(): Int + + /** + * Checks if this value is equal to the given object + * @param other The other value to compare against + */ override fun equals(other: Any?): Boolean + + /** + * Returns the sum of this value and the given value + * @param other The other value to add (i.e., the addend) + */ public operator fun plus(other: BigInteger): BigInteger + + /** + * Returns the difference of this value and the given value + * @param other The value to subtract (i.e., the subtrahend) + */ public operator fun minus(other: BigInteger): BigInteger + + /** + * Returns the [two's complement](https://en.wikipedia.org/wiki/Two%27s_complement) binary representation of this + * value + */ public fun toByteArray(): ByteArray + + /** + * Compare this value to the given value for in/equality + * @param other The value to compare against + */ public override operator fun compareTo(other: BigInteger): Int } 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 96bf20b25a..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 @@ -196,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/common/src/aws/smithy/kotlin/runtime/hashing/Crc32.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Crc32.kt index 84b76b89a0..454c4fc552 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Crc32.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Crc32.kt @@ -11,11 +11,13 @@ public abstract class Crc32Base : HashFunction { override val blockSizeBytes: Int = 4 override val digestSizeBytes: Int = 4 + /** + * Digest the current checksum, returning it as a [UInt] + */ public abstract fun digestValue(): UInt override fun digest(): ByteArray { val x = digestValue() - reset() return byteArrayOf( ((x shr 24) and 0xffu).toByte(), ((x shr 16) and 0xffu).toByte(), diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Crc32c.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Crc32c.kt index da77560226..a6d9e10a61 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Crc32c.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Crc32c.kt @@ -21,7 +21,7 @@ public class Crc32c : Crc32Base() { override fun update(input: ByteArray, offset: Int, length: Int): Unit = md.update(input, offset, length) - override fun digestValue(): UInt = md.getValue().toUInt() + override fun digestValue(): UInt = md.getValue().toUInt().also { reset() } override fun reset(): Unit = md.reset() } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapter.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapter.kt index 178e210a02..eaa85b35b1 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapter.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapter.kt @@ -30,7 +30,7 @@ internal expect class BufferedSinkAdapter(sink: okio.BufferedSink) : SdkBuffered // base class that fills in most of the common implementation, platforms just need to implement the platform specific // part of the interface internal abstract class AbstractBufferedSinkAdapter( - protected val delegate: okio.BufferedSink, + internal val delegate: okio.BufferedSink, ) : SdkBufferedSink { override fun toString(): String = delegate.toString() diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/BuffereredSourceAdapter.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/BuffereredSourceAdapter.kt index 8b92fbbe64..e1842e26dc 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/BuffereredSourceAdapter.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/BuffereredSourceAdapter.kt @@ -32,53 +32,76 @@ internal expect class BufferedSourceAdapter(source: okio.BufferedSource) : SdkBu override fun close() } +/** + * Used to wrap calls to Okio, catching Okio exceptions (e.g. okio.EOFException) and throwing our own (e.g. aws.smithy.kotlin.runtime.io.EOFException). + */ +internal inline fun SdkBufferedSource.wrapOkio(block: SdkBufferedSource.() -> T): T = try { + block() +} catch (e: okio.EOFException) { + throw EOFException("Okio operation failed: ${e.message}", e) +} catch (e: okio.IOException) { + throw IOException("Okio operation failed: ${e.message}", e) +} + // base class that fills in most of the common implementation, platforms just need to implement the platform specific // part of the interface internal abstract class AbstractBufferedSourceAdapter( - protected val delegate: okio.BufferedSource, + internal val delegate: okio.BufferedSource, ) : SdkBufferedSource { override val buffer: SdkBuffer get() = delegate.buffer.toSdk() - override fun skip(byteCount: Long): Unit = delegate.skip(byteCount) + override fun skip(byteCount: Long): Unit = wrapOkio { delegate.skip(byteCount) } - override fun readByte(): Byte = delegate.readByte() + override fun readByte(): Byte = wrapOkio { delegate.readByte() } - override fun readShort(): Short = delegate.readShort() + override fun readShort(): Short = wrapOkio { delegate.readShort() } - override fun readShortLe(): Short = delegate.readShortLe() + override fun readShortLe(): Short = wrapOkio { delegate.readShortLe() } - override fun readLong(): Long = delegate.readLong() + override fun readLong(): Long = wrapOkio { delegate.readLong() } - override fun readLongLe(): Long = delegate.readLongLe() + override fun readLongLe(): Long = wrapOkio { delegate.readLongLe() } - override fun readInt(): Int = delegate.readInt() + override fun readInt(): Int = wrapOkio { delegate.readInt() } - override fun readIntLe(): Int = delegate.readIntLe() + override fun readIntLe(): Int = wrapOkio { delegate.readIntLe() } - override fun readAll(sink: SdkSink): Long = + override fun readAll(sink: SdkSink): Long = wrapOkio { delegate.readAll(sink.toOkio()) + } - override fun read(sink: ByteArray, offset: Int, limit: Int): Int = + override fun read(sink: ByteArray, offset: Int, limit: Int): Int = wrapOkio { delegate.read(sink, offset, limit) + } - override fun read(sink: SdkBuffer, limit: Long): Long = + override fun read(sink: SdkBuffer, limit: Long): Long = wrapOkio { delegate.read(sink.toOkio(), limit) + } - override fun readByteArray(): ByteArray = delegate.readByteArray() + override fun readByteArray(): ByteArray = wrapOkio { delegate.readByteArray() } - override fun readByteArray(byteCount: Long): ByteArray = delegate.readByteArray(byteCount) + override fun readByteArray(byteCount: Long): ByteArray = wrapOkio { + delegate.readByteArray(byteCount) + } - override fun readUtf8(): String = delegate.readUtf8() + override fun readUtf8(): String = wrapOkio { delegate.readUtf8() } - override fun readUtf8(byteCount: Long): String = delegate.readUtf8(byteCount) + override fun readUtf8(byteCount: Long): String = wrapOkio { + delegate.readUtf8(byteCount) + } - override fun peek(): SdkBufferedSource = + override fun peek(): SdkBufferedSource = wrapOkio { delegate.peek().toSdk().buffer() - override fun exhausted(): Boolean = delegate.exhausted() - override fun request(byteCount: Long): Boolean = delegate.request(byteCount) + } + + override fun exhausted(): Boolean = wrapOkio { delegate.exhausted() } + + override fun request(byteCount: Long): Boolean = wrapOkio { + delegate.request(byteCount) + } - override fun require(byteCount: Long): Unit = delegate.require(byteCount) + override fun require(byteCount: Long): Unit = wrapOkio { delegate.require(byteCount) } - override fun close() = delegate.close() + override fun close() = wrapOkio { delegate.close() } } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/Exceptions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/Exceptions.kt index b0dfa89c5f..96d6685ff8 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/Exceptions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/Exceptions.kt @@ -10,8 +10,9 @@ public expect open class IOException(message: String?, cause: Throwable?) : Exce public constructor(message: String?) } -public expect open class EOFException(message: String?) : IOException { +public expect open class EOFException(message: String?, cause: Throwable?) : IOException { public constructor() + public constructor(message: String?) } /** 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 a50259e240..92a159725d 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 @@ -7,6 +7,7 @@ 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.IO import kotlinx.coroutines.withContext /** diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkSource.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkSource.kt index dfb9534627..aefb75dc08 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkSource.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkSource.kt @@ -6,7 +6,15 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.io.internal.JobChannel +import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * A source for reading a stream of bytes (e.g. from file, network, or in-memory buffer). Sources may @@ -43,7 +51,9 @@ public interface SdkSource : Closeable { * Consume the [SdkSource] and pull the entire contents into memory as a [ByteArray]. */ @InternalApi -public expect suspend fun SdkSource.readToByteArray(): ByteArray +public suspend fun SdkSource.readToByteArray(): ByteArray = withContext(SdkDispatchers.IO) { + use { it.buffer().readByteArray() } +} /** * Convert the [SdkSource] to an [SdkByteReadChannel]. Content is read from the source and forwarded @@ -51,8 +61,32 @@ public expect suspend fun SdkSource.readToByteArray(): ByteArray * @param coroutineScope the coroutine scope to use to launch a background reader channel responsible for propagating data * between source and the returned channel */ +@OptIn(DelicateCoroutinesApi::class) @InternalApi -public expect fun SdkSource.toSdkByteReadChannel(coroutineScope: CoroutineScope? = null): SdkByteReadChannel +public fun SdkSource.toSdkByteReadChannel(coroutineScope: CoroutineScope? = null): SdkByteReadChannel { + val source = this + val ch = JobChannel() + val scope = coroutineScope ?: GlobalScope + val job = scope.launch(SdkDispatchers.IO + CoroutineName("sdk-source-reader")) { + val buffer = SdkBuffer() + val result = runCatching { + source.use { + while (true) { + ensureActive() + val rc = source.read(buffer, DEFAULT_BYTE_CHANNEL_MAX_BUFFER_SIZE.toLong()) + if (rc == -1L) break + ch.write(buffer) + } + } + } + + ch.close(result.exceptionOrNull()) + } + + ch.attachJob(job) + + return ch +} /** * Remove exactly [byteCount] bytes from this source and appends them to [sink]. diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/internal/BufferOperations.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/internal/BufferOperations.kt index 1657f933ce..e5cd2a8f70 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/internal/BufferOperations.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/internal/BufferOperations.kt @@ -9,95 +9,108 @@ package aws.smithy.kotlin.runtime.io.internal import aws.smithy.kotlin.runtime.io.* -internal inline fun SdkBuffer.commonSkip(byteCount: Long) = inner.skip(byteCount) +/** + * Used to wrap calls to Okio, catching Okio exceptions (e.g. okio.EOFException) and throwing our own (e.g. aws.smithy.kotlin.runtime.io.EOFException). + */ +internal inline fun SdkBuffer.wrapOkio(block: SdkBuffer.() -> T): T = try { + block() +} catch (e: okio.EOFException) { + throw EOFException("Okio operation failed: ${e.message}", e) +} catch (e: okio.IOException) { + throw IOException("Okio operation failed: ${e.message}", e) +} + +internal inline fun SdkBuffer.commonSkip(byteCount: Long) = wrapOkio { inner.skip(byteCount) } -internal inline fun SdkBuffer.commonReadByte(): Byte = inner.readByte() +internal inline fun SdkBuffer.commonReadByte(): Byte = wrapOkio { inner.readByte() } -internal inline fun SdkBuffer.commonReadShort(): Short = inner.readShort() +internal inline fun SdkBuffer.commonReadShort(): Short = wrapOkio { inner.readShort() } -internal inline fun SdkBuffer.commonReadShortLe(): Short = inner.readShortLe() +internal inline fun SdkBuffer.commonReadShortLe(): Short = wrapOkio { inner.readShortLe() } -internal inline fun SdkBuffer.commonReadLong(): Long = inner.readLong() +internal inline fun SdkBuffer.commonReadLong(): Long = wrapOkio { inner.readLong() } -internal inline fun SdkBuffer.commonReadLongLe(): Long = inner.readLongLe() +internal inline fun SdkBuffer.commonReadLongLe(): Long = wrapOkio { inner.readLongLe() } -internal inline fun SdkBuffer.commonReadInt(): Int = inner.readInt() +internal inline fun SdkBuffer.commonReadInt(): Int = wrapOkio { inner.readInt() } -internal inline fun SdkBuffer.commonReadIntLe(): Int = inner.readIntLe() +internal inline fun SdkBuffer.commonReadIntLe(): Int = wrapOkio { inner.readIntLe() } -internal inline fun SdkBuffer.commonReadAll(sink: SdkSink): Long = - inner.readAll(sink.toOkio()) +internal inline fun SdkBuffer.commonReadAll(sink: SdkSink): Long = wrapOkio { inner.readAll(sink.toOkio()) } internal inline fun SdkBuffer.commonRead(sink: ByteArray, offset: Int, limit: Int): Int = - inner.read(sink, offset, limit) + wrapOkio { inner.read(sink, offset, limit) } internal inline fun SdkBuffer.commonRead(sink: SdkBuffer, limit: Long): Long = - inner.read(sink.inner, limit) + wrapOkio { inner.read(sink.inner, limit) } -internal inline fun SdkBuffer.commonReadByteArray(): ByteArray = inner.readByteArray() +internal inline fun SdkBuffer.commonReadByteArray(): ByteArray = wrapOkio { inner.readByteArray() } -internal inline fun SdkBuffer.commonReadByteArray(byteCount: Long): ByteArray = inner.readByteArray(byteCount) +internal inline fun SdkBuffer.commonReadByteArray(byteCount: Long): ByteArray = wrapOkio { inner.readByteArray(byteCount) } -internal inline fun SdkBuffer.commonReadUtf8(): String = inner.readUtf8() +internal inline fun SdkBuffer.commonReadUtf8(): String = wrapOkio { inner.readUtf8() } -internal inline fun SdkBuffer.commonReadUtf8(byteCount: Long): String = inner.readUtf8(byteCount) +internal inline fun SdkBuffer.commonReadUtf8(byteCount: Long): String = wrapOkio { inner.readUtf8(byteCount) } -internal inline fun SdkBuffer.commonPeek(): SdkBufferedSource = inner.peek().toSdk().buffer() -internal inline fun SdkBuffer.commonExhausted(): Boolean = inner.exhausted() -internal inline fun SdkBuffer.commonRequest(byteCount: Long): Boolean = inner.request(byteCount) +internal inline fun SdkBuffer.commonPeek(): SdkBufferedSource = wrapOkio { inner.peek().toSdk().buffer() } -internal inline fun SdkBuffer.commonRequire(byteCount: Long): Unit = inner.require(byteCount) +internal inline fun SdkBuffer.commonExhausted(): Boolean = wrapOkio { inner.exhausted() } + +internal inline fun SdkBuffer.commonRequest(byteCount: Long): Boolean = wrapOkio { inner.request(byteCount) } + +internal inline fun SdkBuffer.commonRequire(byteCount: Long) = wrapOkio { inner.require(byteCount) } internal inline fun SdkBuffer.commonWrite(source: ByteArray, offset: Int, limit: Int) { - inner.write(source, offset, limit) + wrapOkio { inner.write(source, offset, limit) } } internal inline fun SdkBuffer.commonWrite(source: SdkSource, byteCount: Long) { - inner.write(source.toOkio(), byteCount) + wrapOkio { inner.write(source.toOkio(), byteCount) } } + internal inline fun SdkBuffer.commonWrite(source: SdkBuffer, byteCount: Long) { - inner.write(source.toOkio(), byteCount) + wrapOkio { inner.write(source.toOkio(), byteCount) } } internal inline fun SdkBuffer.commonWriteAll(source: SdkSource): Long = - inner.writeAll(source.toOkio()) + wrapOkio { inner.writeAll(source.toOkio()) } internal inline fun SdkBuffer.commonWriteUtf8(string: String, start: Int, endExclusive: Int) { - inner.writeUtf8(string, start, endExclusive) + wrapOkio { inner.writeUtf8(string, start, endExclusive) } } internal inline fun SdkBuffer.commonWriteByte(x: Byte) { - inner.writeByte(x.toInt()) + wrapOkio { inner.writeByte(x.toInt()) } } internal inline fun SdkBuffer.commonWriteShort(x: Short) { - inner.writeShort(x.toInt()) + wrapOkio { inner.writeShort(x.toInt()) } } internal inline fun SdkBuffer.commonWriteShortLe(x: Short) { - inner.writeShortLe(x.toInt()) + wrapOkio { inner.writeShortLe(x.toInt()) } } internal inline fun SdkBuffer.commonWriteInt(x: Int) { - inner.writeInt(x) + wrapOkio { inner.writeInt(x) } } internal inline fun SdkBuffer.commonWriteIntLe(x: Int) { - inner.writeIntLe(x) + wrapOkio { inner.writeIntLe(x) } } internal inline fun SdkBuffer.commonWriteLong(x: Long) { - inner.writeLong(x) + wrapOkio { inner.writeLong(x) } } internal inline fun SdkBuffer.commonWriteLongLe(x: Long) { - inner.writeLongLe(x) + wrapOkio { inner.writeLongLe(x) } } internal inline fun SdkBuffer.commonFlush() { - inner.flush() + wrapOkio { inner.flush() } } internal inline fun SdkBuffer.commonClose() { - inner.close() + wrapOkio { inner.close() } } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/time/TimestampFormat.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/time/TimestampFormat.kt index 18d005b660..025e1cd668 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/time/TimestampFormat.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/time/TimestampFormat.kt @@ -9,10 +9,10 @@ package aws.smithy.kotlin.runtime.time */ public enum class TimestampFormat { /** - * ISO-8601/RFC5399 timestamp including fractional seconds at microsecond precision (e.g., + * ISO-8601/RFC3339 timestamp including fractional seconds at microsecond precision (e.g., * "2022-04-25T16:44:13.667307Z") * - * Prefers RFC5399 when formatting + * Prefers RFC3339 when formatting */ ISO_8601, @@ -28,7 +28,7 @@ public enum class TimestampFormat { ISO_8601_CONDENSED_DATE, /** - * ISO-8601/RFC5399 timestamp including fractional seconds at arbitrary (i.e., untruncated) precision + * ISO-8601/RFC3339 timestamp including fractional seconds at arbitrary (i.e., untruncated) precision */ ISO_8601_FULL, diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt index 610a7b3405..cfd3dc1cc7 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/Platform.kt @@ -67,6 +67,9 @@ public enum class OsFamily { Windows, Android, Ios, + IpadOs, + TvOs, + WatchOs, Unknown, ; @@ -76,6 +79,9 @@ public enum class OsFamily { Windows -> "windows" Android -> "android" Ios -> "ios" + IpadOs -> "ipados" + TvOs -> "tvos" + WatchOs -> "watchos" Unknown -> "unknown" } } 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..0125eae489 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/compression/GzipTest.kt @@ -0,0 +1,41 @@ +/* + * 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 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) + } +} diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/content/BigIntegerTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/content/BigIntegerTest.kt index 2dc9083025..be78e6c3d8 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/content/BigIntegerTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/content/BigIntegerTest.kt @@ -77,7 +77,6 @@ class BigIntegerTest { "0x123456789abcdef0" to "1311768467463790320", "0x00ffffffffffffffffffffffffffffffec" to "340282366920938463463374607431768211436", "0x81445edf51ddc07216da5621c727bfd379d400f3da08018d45749a" to "-52134902384590238490284023839028330923830129830129301234239834982", - ) tests.forEach { (hex, expected) -> diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/hashing/Crc32Test.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/hashing/Crc32Test.kt index e3b489aba0..8f622fd513 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/hashing/Crc32Test.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/hashing/Crc32Test.kt @@ -20,7 +20,6 @@ class Crc32Test { val input = "foobar" crc.update(input.encodeToByteArray(), 0, input.length) - assertEquals(2666930069U, crc.digestValue()) // checksum of "foobar" assertEquals("nvYflQ==", crc.digest().encodeBase64String()) } @@ -39,9 +38,8 @@ class Crc32Test { val crc = Crc32() val input = "foobar" crc.update(input.encodeToByteArray(), 0, input.length / 2) - assertEquals(2356372769U, crc.digestValue()) // checksum of "foo" crc.update(input.encodeToByteArray(), (input.length - input.length / 2), input.length / 2) - assertEquals(2666930069U, crc.digestValue()) // checksum of "foobar" + assertEquals("nvYflQ==", crc.digest().encodeBase64String()) // checksum of "foobar" } @Test @@ -49,8 +47,7 @@ class Crc32Test { val crc = Crc32() val input = "foobar" crc.update(input.encodeToByteArray(), 0, input.length) - assertEquals(2666930069U, crc.digestValue()) // checksum of "foobar" - crc.digest() + assertEquals("nvYflQ==", crc.digest().encodeBase64String()) // checksum of "foobar" assertEquals(0U, crc.digestValue()) } } diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/hashing/Crc32cTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/hashing/Crc32cTest.kt index 184f62cbd9..0123036e53 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/hashing/Crc32cTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/hashing/Crc32cTest.kt @@ -33,8 +33,6 @@ class Crc32cTest { val input = "foobar" crc.update(input.encodeToByteArray(), 0, input.length / 2) - assertEquals(3485773341U, crc.digestValue()) // checksum of "foo" - crc.update(input.encodeToByteArray(), (input.length - input.length / 2), input.length / 2) assertEquals(224353407U, crc.digestValue()) // checksum of "foobar" } @@ -45,8 +43,7 @@ class Crc32cTest { val input = "foobar" crc.update(input.encodeToByteArray(), 0, input.length) - assertEquals(224353407U, crc.digestValue()) // checksum of "foobar" - assertEquals("DV9cfw==", crc.digest().encodeBase64String()) + assertEquals("DV9cfw==", crc.digest().encodeBase64String()) // checksum of "foobar" } @Test 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 6c6f1884ee..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,6 +4,7 @@ */ package aws.smithy.kotlin.runtime.io +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 bfa47e012d..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,6 +4,7 @@ */ package aws.smithy.kotlin.runtime.io +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/GzipTestUtils.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipTestUtils.kt deleted file mode 100644 index 109844814e..0000000000 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/io/GzipTestUtils.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.io - -/** - * Decompresses a byte array compressed using the gzip format - */ -internal expect fun decompressGzipBytes(bytes: ByteArray): ByteArray diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt new file mode 100644 index 0000000000..3bad624976 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.net + +import kotlinx.coroutines.test.runTest +import kotlin.test.* + +class HostResolverTest { + @Test + fun testResolveLocalhost() = runTest { + val addresses = HostResolver.Default.resolve("localhost") + assertTrue(addresses.isNotEmpty()) + + addresses.forEach { addr -> + assertEquals("localhost", addr.hostname) + + val localHostAddr = when (addr.address) { + is IpV4Addr -> IpV4Addr.LOCALHOST + is IpV6Addr -> IpV6Addr.LOCALHOST + } + assertEquals(addr.address, localHostAddr) + } + } + + @Test + fun testResolveIpv4Address() = runTest { + val addresses = HostResolver.Default.resolve("127.0.0.1") + assertTrue(addresses.isNotEmpty()) + + addresses.forEach { addr -> + assertTrue(addr.address is IpV4Addr) + assertContentEquals(byteArrayOf(127, 0, 0, 1), addr.address.octets) + } + } + + @Test + fun testResolveIpv6Address() = runTest { + val addresses = HostResolver.Default.resolve("::1") + assertTrue(addresses.isNotEmpty()) + + addresses.forEach { addr -> + assertTrue(addr.address is IpV6Addr) + val expectedBytes = ByteArray(16) { 0 } + expectedBytes[15] = 1 + assertContentEquals(expectedBytes, addr.address.octets) + } + } + + @Test + fun testResolveExampleDomain() = runTest { + val addresses = HostResolver.Default.resolve("example.com") + assertNotNull(addresses) + assertTrue(addresses.isNotEmpty()) + + addresses.forEach { addr -> + assertEquals("example.com", addr.hostname) + when (val ip = addr.address) { + is IpV4Addr -> assertEquals(4, ip.octets.size) + is IpV6Addr -> assertEquals(16, ip.octets.size) + } + } + } + + @Test + fun testResolveInvalidDomain() = runTest { + assertFails { + HostResolver.Default.resolve("this-domain-definitely-does-not-exist-12345.local") + } + } + + @Test + fun testNoopMethods() { + // Test no-op methods don't throw + val dummyAddr = HostAddress("test.com", IpV4Addr(ByteArray(4))) + val resolver = HostResolver.Default + resolver.reportFailure(dummyAddr) + resolver.purgeCache(null) + resolver.purgeCache(dummyAddr) + } +} diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/time/InstantTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/time/InstantTest.kt index 4c96d68dc5..c50bbd6d85 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/time/InstantTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/time/InstantTest.kt @@ -17,7 +17,6 @@ import kotlin.time.Duration.Companion.seconds // tests for conversion from a parsed representation into an Instant instance class InstantTest { - /** * Conversion from a string to epoch sec/ns */ @@ -57,8 +56,10 @@ class InstantTest { // leap second - dropped to: 2020-12-31T23:59:59 FromTest("2020-12-31T23:59:60Z", 1609459199, 0), - // midnight - should be 11/5 12AM - FromTest("2020-11-04T24:00:00Z", 1604534400, 0), + + // condensed, date only + FromTest("20250205", 1738713600, 0), + FromTest("20231127", 1701043200, 0), ) @Test @@ -107,7 +108,7 @@ class InstantTest { .fromEpochSeconds(test.sec, test.ns) .format(format) val expected = getter(test) - assertEquals(expected, actual, "test[$idx]: failed to correctly format Instant as $format") + assertEquals(expected, actual, "test[$idx]: failed to correctly format Instant.fromEpochSeconds(${test.sec}, ${test.ns}) as $format") } } } @@ -214,42 +215,26 @@ class InstantTest { assertEquals("1944-06-06T00:00:00Z", timestamp.toString()) } - // Select tests pulled from edge cases/tickets in the V2 Java SDK. - // Always good to learn from others... - class V2JavaSdkTests { - @Test - fun v2JavaSdkTt0031561767() { - val input = "Fri, 16 May 2014 23:56:46 GMT" - val instant: Instant = Instant.fromRfc5322(input) - assertEquals(input, instant.format(TimestampFormat.RFC_5322)) - } + @Test + fun testUntil() { + val untilTests = mapOf( + ("2013-01-01T00:00:00+00:00" to "2014-01-01T00:00:00+00:00") to 365.days, + ("2020-01-01T00:00:00+00:00" to "2021-01-01T00:00:00+00:00") to 366.days, // leap year! + ("2023-10-06T00:00:00+00:00" to "2023-10-06T00:00:00+00:00") to Duration.ZERO, + ("2023-10-06T00:00:00+00:00" to "2023-10-07T00:00:00+00:00") to 1.days, + ("2023-10-06T00:00:00+00:00" to "2023-10-06T01:00:00+00:00") to 1.hours, + ("2023-10-06T00:00:00+00:00" to "2023-10-06T00:01:00+00:00") to 1.minutes, + ("2023-10-06T00:00:00+00:00" to "2023-10-06T00:00:01+00:00") to 1.seconds, + ("2023-10-06T00:00:00+00:00" to "2023-10-06T12:12:12+00:00") to 12.hours + 12.minutes + 12.seconds, + ) - /** - * Tests the Date marshalling and unmarshalling. Asserts that the value is - * same before and after marshalling/unmarshalling - */ - @Test - fun v2JavaSdkUnixTimestampRoundtrip() { - // v2 sdk used currentTimeMillis(), instead we just hard code a value here - // otherwise that would be a JVM specific test since since we do not (yet) have - // a Kotlin MPP way of getting current timestamp. Also obviously not using epoch mill - // but instead just epoch sec. Spirit of the test is the same though - longArrayOf(1595016457, 1L, 0L) - .map { Instant.fromEpochSeconds(0, 0) } - .forEach { instant -> - val serverSpecificDateFormat: String = instant.format(TimestampFormat.EPOCH_SECONDS) - val parsed: Instant = parseEpoch(serverSpecificDateFormat) - assertEquals(instant.epochSeconds, parsed.epochSeconds) - } - } + for ((times, expectedDuration) in untilTests) { + val start = Instant.fromIso8601(times.first) + val end = Instant.fromIso8601(times.second) - // NOTE: There is additional set of edge case tests related to a past issue - // in DateUtilsTest.java in the v2 sdk. Specifically around - // issue 223: https://github.com/aws/aws-sdk-java/issues/233 - // - // (1) - That issue is about round tripping values between SDK versions - // (2) - The input year in those tests is NOT valid and should never have - // been accepted by the parser. + assertEquals(expectedDuration, start.until(end)) + assertEquals(end.until(start), -expectedDuration) + } } @Test @@ -278,26 +263,42 @@ class InstantTest { assertEquals(test.second, actual, "test[$idx]: failed to format offset timestamp in UTC") } } +} +// Select tests pulled from edge cases/tickets in the V2 Java SDK. +// Always good to learn from others... +class V2JavaSdkTests { @Test - fun testUntil() { - val untilTests = mapOf( - ("2013-01-01T00:00:00+00:00" to "2014-01-01T00:00:00+00:00") to 365.days, - ("2020-01-01T00:00:00+00:00" to "2021-01-01T00:00:00+00:00") to 366.days, // leap year! - ("2023-10-06T00:00:00+00:00" to "2023-10-06T00:00:00+00:00") to Duration.ZERO, - ("2023-10-06T00:00:00+00:00" to "2023-10-07T00:00:00+00:00") to 1.days, - ("2023-10-06T00:00:00+00:00" to "2023-10-06T01:00:00+00:00") to 1.hours, - ("2023-10-06T00:00:00+00:00" to "2023-10-06T00:01:00+00:00") to 1.minutes, - ("2023-10-06T00:00:00+00:00" to "2023-10-06T00:00:01+00:00") to 1.seconds, - ("2023-10-06T00:00:00+00:00" to "2023-10-06T12:12:12+00:00") to 12.hours + 12.minutes + 12.seconds, - ) - - for ((times, expectedDuration) in untilTests) { - val start = Instant.fromIso8601(times.first) - val end = Instant.fromIso8601(times.second) + fun v2JavaSdkTt0031561767() { + val input = "Fri, 16 May 2014 23:56:46 GMT" + val instant: Instant = Instant.fromRfc5322(input) + assertEquals(input, instant.format(TimestampFormat.RFC_5322)) + } - assertEquals(expectedDuration, start.until(end)) - assertEquals(end.until(start), -expectedDuration) - } + /** + * Tests the Date marshalling and unmarshalling. Asserts that the value is + * same before and after marshalling/unmarshalling + */ + @Test + fun v2JavaSdkUnixTimestampRoundtrip() { + // v2 sdk used currentTimeMillis(), instead we just hard code a value here + // otherwise that would be a JVM specific test since since we do not (yet) have + // a Kotlin MPP way of getting current timestamp. Also obviously not using epoch mill + // but instead just epoch sec. Spirit of the test is the same though + longArrayOf(1595016457, 1L, 0L) + .map { Instant.fromEpochSeconds(0, 0) } + .forEach { instant -> + val serverSpecificDateFormat: String = instant.format(TimestampFormat.EPOCH_SECONDS) + val parsed: Instant = parseEpoch(serverSpecificDateFormat) + assertEquals(instant.epochSeconds, parsed.epochSeconds) + } } + + // NOTE: There is additional set of edge case tests related to a past issue + // in DateUtilsTest.java in the v2 sdk. Specifically around + // issue 223: https://github.com/aws/aws-sdk-java/issues/233 + // + // (1) - That issue is about round tripping values between SDK versions + // (2) - The input year in those tests is NOT valid and should never have + // been accepted by the parser. } diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt new file mode 100644 index 0000000000..734f788e49 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.util + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SystemPlatformProviderTest { + @Test + fun testReadWriteFile() = runTest { + val ps = PlatformProvider.System + + val tempDir = if (ps.osInfo().family == OsFamily.Windows) { + requireNotNull(ps.getenv("TEMP")) { "%TEMP% unexpectedly null" } + } else { + "/tmp" + } + val path = "$tempDir/testReadWriteFile-${Uuid.random()}.txt" + + val expected = "Hello, File!".encodeToByteArray() + + ps.writeFile(path, expected) + assertTrue(ps.fileExists(path)) + + val actual = ps.readFileOrNull(path) + assertContentEquals(expected, actual) + } + + @Test + fun testGetEnv() = runTest { + val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI + assertNotNull( + envVarKeys.firstNotNullOfOrNull { PlatformProvider.System.getenv(it) }, + ) + + assertNull(PlatformProvider.System.getenv("THIS_ENV_VAR_IS_NOT_SET")) + } + + @Test + fun testGetAllEnvVars() = runTest { + val allEnv = PlatformProvider.System.getAllEnvVars() + assertTrue(allEnv.isNotEmpty()) + + val envVarKeys = listOf("PATH", "USERPROFILE") // PATH is not set on Windows CI + assertTrue( + envVarKeys.any { allEnv.contains(it) }, + ) + } + + @Test + fun testOsInfo() = runTest { + val osInfo = PlatformProvider.System.osInfo() + assertNotEquals(OsFamily.Unknown, osInfo.family) + } +} 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/src/aws/smithy/kotlin/runtime/content/BigDecimalJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigDecimalJVM.kt index 9fb5d4efce..a7de1ab953 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigDecimalJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigDecimalJVM.kt @@ -4,33 +4,52 @@ */ package aws.smithy.kotlin.runtime.content -public actual class BigDecimal actual constructor(public val value: String) : +import java.math.BigDecimal as JvmBigDecimal + +public actual class BigDecimal private constructor(private val delegate: JvmBigDecimal) : Number(), Comparable { - private val delegate = java.math.BigDecimal(value) - public actual constructor(mantissa: BigInteger, exponent: Int) : this( - java.math.BigDecimal( - java.math.BigInteger(mantissa.toString()), - exponent, - ).toPlainString(), - ) + private companion object { + /** + * Returns a new or existing [BigDecimal] wrapper for the given delegate [value] + * @param value The delegate value to wrap + * @param left A candidate wrapper which may already contain [value] + * @param right A candidate wrapper which may already contain [value] + */ + fun coalesceOrCreate(value: JvmBigDecimal, left: BigDecimal, right: BigDecimal): BigDecimal = when (value) { + left.delegate -> left + right.delegate -> right + else -> BigDecimal(value) + } + } + + public actual constructor(value: String) : this(JvmBigDecimal(value)) + public actual constructor(mantissa: BigInteger, exponent: Int) : this(JvmBigDecimal(mantissa.delegate, exponent)) public actual fun toPlainString(): String = delegate.toPlainString() - actual override fun toByte(): Byte = delegate.toByte() - actual override fun toDouble(): Double = delegate.toDouble() - actual override fun toFloat(): Float = delegate.toFloat() - actual override fun toInt(): Int = delegate.toInt() - actual override fun toLong(): Long = delegate.toLong() - actual override fun toShort(): Short = delegate.toShort() + public actual override fun toString(): String = delegate.toString() + public actual override fun toByte(): Byte = delegate.toByte() + public actual override fun toDouble(): Double = delegate.toDouble() + public actual override fun toFloat(): Float = delegate.toFloat() + public actual override fun toInt(): Int = delegate.toInt() + public actual override fun toLong(): Long = delegate.toLong() + public actual override fun toShort(): Short = delegate.toShort() - actual override fun equals(other: Any?): Boolean = other is BigDecimal && delegate == other.delegate + public actual override fun equals(other: Any?): Boolean = other is BigDecimal && delegate == other.delegate + public actual override fun hashCode(): Int = 31 + delegate.hashCode() public actual val mantissa: BigInteger - get() = BigInteger(delegate.unscaledValue().toString()) + get() = BigInteger(delegate.unscaledValue()) public actual val exponent: Int get() = delegate.scale() + public actual operator fun plus(other: BigDecimal): BigDecimal = + coalesceOrCreate(delegate + other.delegate, this, other) + + public actual operator fun minus(other: BigDecimal): BigDecimal = + coalesceOrCreate(delegate - other.delegate, this, other) + actual override fun compareTo(other: BigDecimal): Int = delegate.compareTo(other.delegate) } diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigIntegerJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigIntegerJVM.kt index e89d12c9c4..2f864710b3 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigIntegerJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/BigIntegerJVM.kt @@ -4,12 +4,28 @@ */ package aws.smithy.kotlin.runtime.content -public actual class BigInteger actual constructor(public val value: String) : +import java.math.BigInteger as JvmBigInteger + +public actual class BigInteger internal constructor(internal val delegate: JvmBigInteger) : Number(), Comparable { - private val delegate = java.math.BigInteger(value) - public actual constructor(bytes: ByteArray) : this(java.math.BigInteger(bytes).toString()) + private companion object { + /** + * Returns a new or existing [BigInteger] wrapper for the given delegate [value] + * @param value The delegate value to wrap + * @param left A candidate wrapper which may already contain [value] + * @param right A candidate wrapper which may already contain [value] + */ + fun coalesceOrCreate(value: JvmBigInteger, left: BigInteger, right: BigInteger): BigInteger = when (value) { + left.delegate -> left + right.delegate -> right + else -> BigInteger(value) + } + } + + public actual constructor(value: String) : this(JvmBigInteger(value)) + public actual constructor(bytes: ByteArray) : this(JvmBigInteger(bytes)) public actual override fun toByte(): Byte = delegate.toByte() public actual override fun toLong(): Long = delegate.toLong() @@ -17,12 +33,18 @@ public actual class BigInteger actual constructor(public val value: String) : public actual override fun toInt(): Int = delegate.toInt() public actual override fun toFloat(): Float = delegate.toFloat() public actual override fun toDouble(): Double = delegate.toDouble() - public actual override fun toString(): String = delegate.toString() - public actual override fun hashCode(): Int = delegate.hashCode() + public actual override fun toString(): String = toString(10) + public actual fun toString(radix: Int): String = delegate.toString(radix) + + public actual override fun hashCode(): Int = 17 + delegate.hashCode() public actual override fun equals(other: Any?): Boolean = other is BigInteger && delegate == other.delegate - public actual operator fun plus(other: BigInteger): BigInteger = BigInteger((delegate + other.delegate).toString()) - public actual operator fun minus(other: BigInteger): BigInteger = BigInteger((delegate - other.delegate).toString()) + public actual operator fun plus(other: BigInteger): BigInteger = + coalesceOrCreate(delegate + other.delegate, this, other) + + public actual operator fun minus(other: BigInteger): BigInteger = + coalesceOrCreate(delegate - other.delegate, this, other) + public actual override operator fun compareTo(other: BigInteger): Int = delegate.compareTo(other.delegate) public actual fun toByteArray(): ByteArray = delegate.toByteArray() } 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 5647ac15af..9d1a03d49a 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 @@ -96,7 +96,14 @@ public suspend fun ByteStream.writeToFile(path: Path): Long = writeToFile(path.t public fun ByteStream.toInputStream(): InputStream = when (this) { is ByteStream.Buffer -> ByteArrayInputStream(bytes()) is ByteStream.ChannelStream -> readFrom().toInputStream() - is ByteStream.SourceStream -> readFrom().buffer().inputStream() + is ByteStream.SourceStream -> { + val buffer = (readFrom().buffer()) + when (buffer) { + is SdkBuffer -> buffer.inputStream() + is BufferedSourceAdapter -> buffer.inputStream() + else -> throw IllegalStateException("Buffer class ${buffer::class.simpleName} could not be converted to an InputStream") + } + } } /** diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/hashing/Crc32JVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/hashing/Crc32JVM.kt index 0c42a47434..5afae09bad 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/hashing/Crc32JVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/hashing/Crc32JVM.kt @@ -13,7 +13,7 @@ public actual class Crc32 : Crc32Base() { actual override fun update(input: ByteArray, offset: Int, length: Int): Unit = md.update(input, offset, length) - actual override fun digestValue(): UInt = md.value.toUInt() + actual override fun digestValue(): UInt = md.value.toUInt().also { reset() } actual override fun reset(): Unit = md.reset() } diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapterJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapterJVM.kt deleted file mode 100644 index 404dd6d078..0000000000 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapterJVM.kt +++ /dev/null @@ -1,20 +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.io.OutputStream -import java.nio.ByteBuffer - -internal actual class BufferedSinkAdapter actual constructor( - sink: okio.BufferedSink, -) : AbstractBufferedSinkAdapter(sink), - SdkBufferedSink { - override fun write(src: ByteBuffer): Int = delegate.write(src) - - override fun isOpen(): Boolean = delegate.isOpen - - override fun outputStream(): OutputStream = delegate.outputStream() -} diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/BufferedSourceAdapterJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/BufferedSourceAdapterJVM.kt deleted file mode 100644 index e7b1c3d759..0000000000 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/BufferedSourceAdapterJVM.kt +++ /dev/null @@ -1,21 +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.io.InputStream -import java.nio.ByteBuffer - -internal actual class BufferedSourceAdapter actual constructor( - source: okio.BufferedSource, -) : AbstractBufferedSourceAdapter(source), - SdkBufferedSource { - - override fun read(dst: ByteBuffer): Int = delegate.read(dst) - - override fun isOpen(): Boolean = delegate.isOpen - - override fun inputStream(): InputStream = delegate.inputStream() -} diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/ExceptionsJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/ExceptionsJVM.kt index 82d28aaac0..6d01916a93 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/ExceptionsJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/ExceptionsJVM.kt @@ -7,4 +7,14 @@ package aws.smithy.kotlin.runtime.io public actual typealias IOException = java.io.IOException -public actual typealias EOFException = java.io.EOFException +public actual open class EOFException actual constructor( + message: String?, + cause: Throwable?, +) : java.io.EOFException(message) { + init { + initCause(cause) + } + + public actual constructor() : this(null, null) + public actual constructor(message: String?) : this(message, null) +} diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/JavaIO.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/JavaIO.kt index e7578bfea4..c035aa9a4c 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/JavaIO.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/JavaIO.kt @@ -9,6 +9,7 @@ import aws.smithy.kotlin.runtime.io.internal.toSdk import java.io.File import java.io.InputStream import java.io.OutputStream +import java.nio.ByteBuffer import java.nio.file.Path import okio.sink as okioSink import okio.source as okioSource @@ -59,3 +60,20 @@ public fun InputStream.source(): SdkSource = okioSource().toSdk() * Create a new [SdkSource] that reads from this [InputStream] */ public fun OutputStream.sink(): SdkSink = okioSink().toSdk() + +// BufferedSinkAdapter +internal fun BufferedSinkAdapter.outputStream(): OutputStream = delegate.outputStream() +internal fun BufferedSinkAdapter.write(src: ByteBuffer): Int = delegate.write(src) +internal fun BufferedSinkAdapter.isOpen(): Boolean = delegate.isOpen + +// BufferedSourceAdapter +internal fun BufferedSourceAdapter.read(dst: ByteBuffer): Int = delegate.read(dst) +internal fun BufferedSourceAdapter.isOpen(): Boolean = delegate.isOpen +internal fun BufferedSourceAdapter.inputStream(): InputStream = delegate.inputStream() + +// SdkBuffer +public fun SdkBuffer.read(dst: ByteBuffer): Int = wrapOkio { inner.read(dst) } +public fun SdkBuffer.write(src: ByteBuffer): Int = wrapOkio { inner.write(src) } +public fun SdkBuffer.isOpen(): Boolean = inner.isOpen +public fun SdkBuffer.inputStream(): InputStream = inner.inputStream() +public fun SdkBuffer.outputStream(): OutputStream = inner.outputStream() diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkBufferedSinkJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkBufferedSinkJVM.kt deleted file mode 100644 index 6a44c557a3..0000000000 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkBufferedSinkJVM.kt +++ /dev/null @@ -1,109 +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.io.OutputStream -import java.nio.channels.WritableByteChannel -import kotlin.jvm.Throws - -/** - * A sink that keeps a buffer internally so that callers can do small writes without - * a performance penalty. - */ -public actual sealed interface SdkBufferedSink : - SdkSink, - WritableByteChannel { - /** - * The underlying buffer for this sink - */ - public actual val buffer: SdkBuffer - - /** - * Write [limit] bytes from [source] starting at [offset] - */ - @Throws(IOException::class) - public actual fun write(source: ByteArray, offset: Int, limit: Int): Unit - - /** - * Write all bytes from [source] to this sink. - * @return the number of bytes read which will be 0 if [source] is exhausted - */ - @Throws(IOException::class) - public actual fun writeAll(source: SdkSource): Long - - /** - * Removes [byteCount] bytes from [source] and writes them to this sink. - */ - @Throws(IOException::class) - public actual fun write(source: SdkSource, byteCount: Long): Unit - - /** - * Write UTF8-bytes of [string] to this sink starting at [start] index up to [endExclusive] index. - */ - @Throws(IOException::class) - public actual fun writeUtf8(string: String, start: Int, endExclusive: Int): Unit - - /** - * Writes byte [x] to this sink - */ - @Throws(IOException::class) - public actual fun writeByte(x: Byte): Unit - - /** - * Writes short [x] as a big-endian bytes to this sink - */ - @Throws(IOException::class) - public actual fun writeShort(x: Short): Unit - - /** - * Writes short [x] as a little-endian bytes to this sink - */ - @Throws(IOException::class) - public actual fun writeShortLe(x: Short): Unit - - /** - * Writes int [x] as a big-endian bytes to this sink - */ - @Throws(IOException::class) - public actual fun writeInt(x: Int): Unit - - /** - * Writes int [x] as a little-endian bytes to this sink - */ - @Throws(IOException::class) - public actual fun writeIntLe(x: Int): Unit - - /** - * Writes long [x] as a big-endian bytes to this sink - */ - @Throws(IOException::class) - public actual fun writeLong(x: Long): Unit - - /** - * Writes long [x] as a little-endian bytes to this sink - */ - @Throws(IOException::class) - public actual fun writeLongLe(x: Long): Unit - - /** - * Return an output stream that writes to this sink - */ - public fun outputStream(): OutputStream - - /** - * Writes all buffered data to the underlying sink. - */ - @Throws(IOException::class) - actual override fun flush(): Unit - - /** - * Writes all buffered data to the underlying sink. Like flush, but weaker (ensures data is pushed to the - * underlying sink but not necessarily all the way down the chain like [flush] does). Call before this sink - * goes out of scope to ensure any buffered data eventually gets to its final destination - */ - @Throws(IOException::class) - public actual fun emit(): Unit -} diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkBufferedSourceJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkBufferedSourceJVM.kt deleted file mode 100644 index 076502f7c5..0000000000 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkBufferedSourceJVM.kt +++ /dev/null @@ -1,144 +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.io.InputStream -import java.nio.channels.ReadableByteChannel - -public actual sealed interface SdkBufferedSource : - SdkSource, - ReadableByteChannel { - - /** - * The underlying buffer for this source - */ - public actual val buffer: SdkBuffer - - /** - * Discards [byteCount] bytes from this source. Throws [IOException] if source is exhausted before [byteCount] - * bytes can be discarded. - */ - @Throws(IOException::class) - public actual fun skip(byteCount: Long) - - /** - * Read a single byte from this source and return it - */ - @Throws(IOException::class) - public actual fun readByte(): Byte - - /** - * Read two bytes in big-endian order from this source and returns them as a short. - */ - @Throws(IOException::class) - public actual fun readShort(): Short - - /** - * Read two bytes in little-endian order from this source and returns them as a short. - */ - @Throws(IOException::class) - public actual fun readShortLe(): Short - - /** - * Read eight bytes in big-endian order from this source and returns them as a long. - */ - @Throws(IOException::class) - public actual fun readLong(): Long - - /** - * Read eight bytes in little-endian order from this source and returns them as a long. - */ - @Throws(IOException::class) - public actual fun readLongLe(): Long - - /** - * Read four bytes in big-endian order from this source and returns them as an int. - */ - @Throws(IOException::class) - public actual fun readInt(): Int - - /** - * Read four bytes in little-endian order from this source and returns them as an int. - */ - @Throws(IOException::class) - public actual fun readIntLe(): Int - - /** - * Reads all bytes from this and appends them to [sink]. Returns - * the total number of bytes written which will be 0 if this source - * is exhausted. - */ - @Throws(IOException::class) - public actual fun readAll(sink: SdkSink): Long - - /** - * Read up to [limit] bytes and write them to [sink] starting at [offset] - */ - @Throws(IOException::class) - public actual fun read(sink: ByteArray, offset: Int, limit: Int): Int - - /** - * Reads all bytes from this source and returns them as a byte array - * - * **Caution** This may pull a large amount of data into memory, only do this if you are sure - * the contents fit into memory. Throws [IllegalArgumentException] if the buffer size exceeds [Int.MAX_VALUE]. - */ - @Throws(IOException::class) - public actual fun readByteArray(): ByteArray - - /** - * Reads [byteCount] bytes from this source and returns them as a byte array - */ - @Throws(IOException::class) - public actual fun readByteArray(byteCount: Long): ByteArray - - /** - * Reads all bytes from this source, decodes them as UTF-8, and returns the string. - * - * **Caution** This may pull a large amount of data into memory, only do this if you are sure - * the contents fit into memory. Throws [IllegalArgumentException] if the buffer size exceeds [Int.MAX_VALUE]. - */ - @Throws(IOException::class) - public actual fun readUtf8(): String - - /** - * Reads [byteCount] bytes from this source, decodes them as UTF-8, and returns the string. - */ - @Throws(IOException::class) - public actual fun readUtf8(byteCount: Long): String - - /** - * Get an input stream that reads from this source - */ - public fun inputStream(): InputStream - - /** - * Returns a new [SdkBufferedSource] that can read data from this source - * without consuming it. The returned source becomes invalid once this source is next - * read or closed. - */ - public actual fun peek(): SdkBufferedSource - - /** - * Returns true if there are no more bytes in this source. This will block until there are bytes - * to read or the source is definitely exhausted. - */ - public actual fun exhausted(): Boolean - - /** - * Returns true when the buffer contains at least [byteCount] bytes. False if the source - * is exhausted before the requested number of bytes could be read - */ - @Throws(IOException::class) - public actual fun request(byteCount: Long): Boolean - - /** - * Returns when the buffer contains at least [byteCount] bytes or throws [EOFException] - * if the source is exhausted before the requested number of bytes could be read - */ - @Throws(IOException::class) - public actual fun require(byteCount: Long): Unit -} diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannelJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannelJVM.kt index 5afef01745..2986b998a3 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannelJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannelJVM.kt @@ -6,7 +6,6 @@ package aws.smithy.kotlin.runtime.io import kotlinx.coroutines.runBlocking import java.io.InputStream -import java.util.* /** * Create a blocking [InputStream] that blocks everytime the channel suspends at [SdkByteReadChannel.read] diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkSourceJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkSourceJVM.kt deleted file mode 100644 index 2eda1fd3f7..0000000000 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkSourceJVM.kt +++ /dev/null @@ -1,42 +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 aws.smithy.kotlin.runtime.InternalApi -import aws.smithy.kotlin.runtime.io.internal.JobChannel -import kotlinx.coroutines.* - -@InternalApi -public actual suspend fun SdkSource.readToByteArray(): ByteArray = withContext(Dispatchers.IO) { - use { it.buffer().readByteArray() } -} - -@InternalApi -@OptIn(DelicateCoroutinesApi::class) -public actual fun SdkSource.toSdkByteReadChannel(coroutineScope: CoroutineScope?): SdkByteReadChannel { - val source = this - val ch = JobChannel() - val scope = coroutineScope ?: GlobalScope - val job = scope.launch(Dispatchers.IO + CoroutineName("sdk-source-reader")) { - val buffer = SdkBuffer() - val result = runCatching { - source.use { - while (true) { - ensureActive() - val rc = source.read(buffer, DEFAULT_BYTE_CHANNEL_MAX_BUFFER_SIZE.toLong()) - if (rc == -1L) break - ch.write(buffer) - } - } - } - - ch.close(result.exceptionOrNull()) - } - - ch.attachJob(job) - - return ch -} diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/time/InstantJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/time/InstantJVM.kt index b90a57de82..3f0f1459fa 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/time/InstantJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/time/InstantJVM.kt @@ -25,7 +25,9 @@ import kotlin.time.Duration import kotlin.time.toKotlinDuration import java.time.Duration as jtDuration import java.time.Instant as jtInstant +import kotlinx.datetime.Instant as KtInstant +// FIXME Consider making this multiplatform (`common`) using kotlinx.datetime public actual class Instant(internal val value: jtInstant) : Comparable { public actual val epochSeconds: Long get() = value.epochSecond @@ -135,12 +137,12 @@ public actual class Instant(internal val value: jtInstant) : Comparable /** * Create an [Instant] with the minimum possible value */ - public actual val MIN_VALUE: Instant = Instant(jtInstant.MIN) + public actual val MIN_VALUE: Instant = fromEpochMilliseconds(KtInstant.DISTANT_PAST.toEpochMilliseconds()) /** * Create an [Instant] with the maximum possible value */ - public actual val MAX_VALUE: Instant = Instant(jtInstant.MAX) + public actual val MAX_VALUE: Instant = fromEpochMilliseconds(KtInstant.DISTANT_FUTURE.toEpochMilliseconds()) } } 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 1d79af8102..0000000000 --- a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsJVM.kt +++ /dev/null @@ -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 byte array compressed using the gzip format - */ -internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray = - GZIPInputStream(bytes.inputStream()).use { it.readBytes() } diff --git a/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapterJvmAndNative.kt b/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapterJvmAndNative.kt new file mode 100644 index 0000000000..7205c8d6a1 --- /dev/null +++ b/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapterJvmAndNative.kt @@ -0,0 +1,12 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.io + +import aws.smithy.kotlin.runtime.InternalApi + +@InternalApi +internal actual class BufferedSinkAdapter actual constructor(sink: okio.BufferedSink) : + AbstractBufferedSinkAdapter(sink), + SdkBufferedSink diff --git a/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/BufferedSourceAdapterJvmAndNative.kt b/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/BufferedSourceAdapterJvmAndNative.kt new file mode 100644 index 0000000000..437b018606 --- /dev/null +++ b/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/BufferedSourceAdapterJvmAndNative.kt @@ -0,0 +1,9 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.io + +internal actual class BufferedSourceAdapter actual constructor(source: okio.BufferedSource) : + AbstractBufferedSourceAdapter(source), + SdkBufferedSource diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkBufferJVM.kt b/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/SdkBufferJvmAndNative.kt similarity index 81% rename from runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkBufferJVM.kt rename to runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/SdkBufferJvmAndNative.kt index 3fc01858e4..c0472cd857 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/io/SdkBufferJVM.kt +++ b/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/SdkBufferJvmAndNative.kt @@ -2,13 +2,9 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ - package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.io.internal.* -import java.io.InputStream -import java.io.OutputStream -import java.nio.ByteBuffer public actual class SdkBuffer : SdkBufferedSource, @@ -37,6 +33,36 @@ public actual class SdkBuffer : return inner == other.inner } + actual override fun write(source: SdkBuffer, byteCount: Long): Unit = commonWrite(source, byteCount) + + actual override fun write(source: ByteArray, offset: Int, limit: Int): Unit = commonWrite(source, offset, limit) + + actual override fun write(source: SdkSource, byteCount: Long): Unit = commonWrite(source, byteCount) + + actual override fun writeAll(source: SdkSource): Long = commonWriteAll(source) + + actual override fun writeUtf8(string: String, start: Int, endExclusive: Int): Unit = commonWriteUtf8(string, start, endExclusive) + + actual override fun writeByte(x: Byte): Unit = commonWriteByte(x) + + actual override fun writeShort(x: Short): Unit = commonWriteShort(x) + + actual override fun writeShortLe(x: Short): Unit = commonWriteShortLe(x) + + actual override fun writeInt(x: Int): Unit = commonWriteInt(x) + + actual override fun writeIntLe(x: Int): Unit = commonWriteIntLe(x) + + actual override fun writeLong(x: Long): Unit = commonWriteLong(x) + + actual override fun writeLongLe(x: Long): Unit = commonWriteLongLe(x) + + actual override fun flush(): Unit = commonFlush() + + actual override fun emit() { + wrapOkio { inner.emit() } + } + actual override fun skip(byteCount: Long): Unit = commonSkip(byteCount) actual override fun readByte(): Byte = commonReadByte() @@ -55,13 +81,7 @@ public actual class SdkBuffer : actual override fun readAll(sink: SdkSink): Long = commonReadAll(sink) - actual override fun read(sink: ByteArray, offset: Int, limit: Int): Int = - commonRead(sink, offset, limit) - - actual override fun read(sink: SdkBuffer, limit: Long): Long = - commonRead(sink, limit) - - override fun read(dst: ByteBuffer): Int = inner.read(dst) + actual override fun read(sink: ByteArray, offset: Int, limit: Int): Int = commonRead(sink, offset, limit) actual override fun readByteArray(): ByteArray = commonReadByteArray() @@ -74,48 +94,12 @@ public actual class SdkBuffer : actual override fun peek(): SdkBufferedSource = commonPeek() actual override fun exhausted(): Boolean = commonExhausted() + actual override fun request(byteCount: Long): Boolean = commonRequest(byteCount) actual override fun require(byteCount: Long): Unit = commonRequire(byteCount) - actual override fun write(source: ByteArray, offset: Int, limit: Int): Unit = - commonWrite(source, offset, limit) - - actual override fun write(source: SdkSource, byteCount: Long): Unit = - commonWrite(source, byteCount) - - actual override fun write(source: SdkBuffer, byteCount: Long): Unit = - commonWrite(source, byteCount) - - override fun write(src: ByteBuffer): Int = inner.write(src) - - actual override fun writeAll(source: SdkSource): Long = commonWriteAll(source) - - actual override fun writeUtf8(string: String, start: Int, endExclusive: Int): Unit = - commonWriteUtf8(string, start, endExclusive) - - actual override fun writeByte(x: Byte): Unit = commonWriteByte(x) - - actual override fun writeShort(x: Short): Unit = commonWriteShort(x) - - actual override fun writeShortLe(x: Short): Unit = commonWriteShortLe(x) + actual override fun read(sink: SdkBuffer, limit: Long): Long = commonRead(sink, limit) - actual override fun writeInt(x: Int): Unit = commonWriteInt(x) - - actual override fun writeIntLe(x: Int): Unit = commonWriteIntLe(x) - - actual override fun writeLong(x: Long): Unit = commonWriteLong(x) - - actual override fun writeLongLe(x: Long): Unit = commonWriteLongLe(x) - - actual override fun flush(): Unit = commonFlush() - - actual override fun emit() { - inner.emit() - } actual override fun close(): Unit = commonClose() - override fun isOpen(): Boolean = inner.isOpen - - override fun inputStream(): InputStream = inner.inputStream() - override fun outputStream(): OutputStream = inner.outputStream() } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkBufferedSinkNative.kt b/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/SdkBufferedSinkJvmAndNative.kt similarity index 100% rename from runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkBufferedSinkNative.kt rename to runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/SdkBufferedSinkJvmAndNative.kt diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkBufferedSourceNative.kt b/runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/SdkBufferedSourceJvmAndNative.kt similarity index 100% rename from runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkBufferedSourceNative.kt rename to runtime/runtime-core/jvmAndNative/src/aws/smithy/kotlin/runtime/io/SdkBufferedSourceJvmAndNative.kt diff --git a/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt new file mode 100644 index 0000000000..f6ca8da166 --- /dev/null +++ b/runtime/runtime-core/linuxX64/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderLinuxX64Test.kt @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.util + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class SystemPlatformProviderLinuxX64Test { + @Test + fun testOsInfo() = runTest { + val osInfo = PlatformProvider.System.osInfo() + assertEquals(OsFamily.Linux, osInfo.family) + } +} diff --git a/runtime/runtime-core/mingw/src/aws/smithy/kotlin/runtime/net/DefaultHostResolverMingw.kt b/runtime/runtime-core/mingw/src/aws/smithy/kotlin/runtime/net/DefaultHostResolverMingw.kt new file mode 100644 index 0000000000..75459c8912 --- /dev/null +++ b/runtime/runtime-core/mingw/src/aws/smithy/kotlin/runtime/net/DefaultHostResolverMingw.kt @@ -0,0 +1,101 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.net + +import kotlinx.cinterop.UnsafeNumber +import kotlinx.cinterop.alloc +import kotlinx.cinterop.allocPointerTo +import kotlinx.cinterop.invoke +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pointed +import kotlinx.cinterop.ptr +import kotlinx.cinterop.refTo +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.toKString +import kotlinx.cinterop.value +import platform.posix.AF_INET +import platform.posix.AF_INET6 +import platform.posix.AF_UNSPEC +import platform.posix.SOCK_STREAM +import platform.posix.WSADATA +import platform.posix.memcpy +import platform.posix.sockaddr +import platform.posix.sockaddr_in +import platform.windows.AI_PASSIVE +import platform.windows.WSACleanup +import platform.windows.WSAStartup +import platform.windows.addrinfo +import platform.windows.freeaddrinfo +import platform.windows.gai_strerror +import platform.windows.getaddrinfo +import platform.windows.sockaddr_in6 + +internal actual object DefaultHostResolver : HostResolver { + actual override suspend fun resolve(hostname: String): List = memScoped { + // Version format specified in https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsastartup + val wsaMajorVersion = 1u + val wsaMinorVersion = 1u + val wsaVersion = (wsaMajorVersion shl 8 or wsaMinorVersion).toUShort() + + val wsaInfo = alloc() + val wsaResult = WSAStartup(wsaVersion, wsaInfo.ptr) + check(wsaResult == 0) { "Failed to initialize Windows Sockets (error code $wsaResult)" } + + try { + val hints = alloc().apply { + ai_family = AF_UNSPEC // Allow both IPv4 and IPv6 + ai_socktype = SOCK_STREAM // TCP stream sockets + ai_flags = AI_PASSIVE // For wildcard IP address + } + + val result = allocPointerTo() + + try { + // Perform the DNS lookup + val status = getaddrinfo(hostname, null, hints.ptr, result.ptr) + check(status == 0) { "Failed to resolve host $hostname: ${gai_strerror?.invoke(status)?.toKString()}" } + + return generateSequence(result.value) { it.pointed.ai_next } + .map { it.pointed.ai_addr!!.pointed.toIpAddr() } + .map { HostAddress(hostname, it) } + .toList() + } finally { + freeaddrinfo(result.value) + } + } finally { + WSACleanup() + } + } + + @OptIn(UnsafeNumber::class) + private fun sockaddr.toIpAddr(): IpAddr { + val (size, addrPtr, constructor) = when (sa_family.toInt()) { + AF_INET -> Triple( + 4, + reinterpret().sin_addr.ptr, + { bytes: ByteArray -> IpV4Addr(bytes) }, + ) + AF_INET6 -> Triple( + 16, + reinterpret().sin6_addr.ptr, + { bytes: ByteArray -> IpV6Addr(bytes) }, + ) + else -> throw IllegalArgumentException("Unsupported sockaddr family $sa_family") + } + + val ipBytes = ByteArray(size) + memcpy(ipBytes.refTo(0), addrPtr, size.toULong()) + return constructor(ipBytes) + } + + actual override fun reportFailure(addr: HostAddress) { + // No-op, same as JVM implementation + } + + actual override fun purgeCache(addr: HostAddress?) { + // No-op, same as JVM implementation + } +} diff --git a/runtime/runtime-core/mingw/src/aws/smithy/kotlin/runtime/util/SystemDefaultProviderMingw.kt b/runtime/runtime-core/mingw/src/aws/smithy/kotlin/runtime/util/SystemDefaultProviderMingw.kt new file mode 100644 index 0000000000..0addf5c419 --- /dev/null +++ b/runtime/runtime-core/mingw/src/aws/smithy/kotlin/runtime/util/SystemDefaultProviderMingw.kt @@ -0,0 +1,111 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.util + +import aws.smithy.kotlin.native.winver.* +import kotlinx.cinterop.* +import platform.posix.environ +import platform.posix.memcpy + +public actual object SystemDefaultProvider : SystemDefaultProviderBase() { + actual override val filePathSeparator: String = "\\" + actual override fun osInfo(): OperatingSystem = OperatingSystem(OsFamily.Windows, osVersionFromKernel()) + + actual override fun getAllEnvVars(): Map = memScoped { + generateSequence(0) { it + 1 } + .map { idx -> environ.get(idx)?.toKString() } + .takeWhile { it != null } + .associate { env -> + val parts = env?.split("=", limit = 2) + check(parts?.size == 2) { "Environment entry \"$env\" is malformed" } + parts[0] to parts[1] + } + } +} + +// The functions below are adapted from C++ SDK: +// https://github.com/aws/aws-sdk-cpp/blob/0e6085bf0dd9a1cb1f27d101c4cf2db6ade6f307/src/aws-cpp-sdk-core/source/platform/windows/OSVersionInfo.cpp#L49-L106 + +private val wordHexFormat = HexFormat { + upperCase = false + number { + removeLeadingZeros = true + minLength = 4 + } +} + +private data class LangCodePage( + val language: UShort, + val codePage: UShort, +) + +private fun osVersionFromKernel(): String? = memScoped { + withFileVersionInfo("Kernel32.dll") { versionInfoPtr -> + getLangCodePage(versionInfoPtr)?.let { langCodePage -> + getProductVersion(versionInfoPtr, langCodePage) + } + } +} + +private inline fun withFileVersionInfo(fileName: String, block: (CPointer>) -> R?): R? { + val blobSize = GetFileVersionInfoSizeW(fileName, null) + val blob = ByteArray(blobSize.convert()) + blob.usePinned { pinned -> + val result = GetFileVersionInfoW(fileName, 0u, blobSize, pinned.addressOf(0)) + return if (result == 0) { + null + } else { + block(pinned.addressOf(0)) + } + } +} + +private fun MemScope.getLangCodePage(versionInfoPtr: CPointer>): LangCodePage? { + // Get _any_ language pack and codepage since they should all have the same version + val langAndCodePagePtr = alloc() + val codePageSize = alloc() + val result = VerQueryValueW( + versionInfoPtr, + """\VarFileInfo\Translation""", + langAndCodePagePtr.ptr, + codePageSize.ptr, + ) + + return if (result == 0) { + null + } else { + val langAndCodePage = langAndCodePagePtr.value!!.reinterpret().pointed.value + val language = (langAndCodePage and 0x0000ffffu).toUShort() // low WORD + val codePage = (langAndCodePage and 0xffff0000u shr 16).toUShort() // high WORD + LangCodePage(language, codePage) + } +} + +private fun MemScope.getProductVersion(versionInfoPtr: CPointer>, langCodePage: LangCodePage): String? { + val versionId = buildString { + // Something like: \StringFileInfo\04090fb0\ProductVersion + append("""\StringFileInfo\""") + append(langCodePage.language.toHexString(wordHexFormat)) + append(langCodePage.codePage.toHexString(wordHexFormat)) + append("""\ProductVersion""") + } + + // Get the block corresponding to versionId + val block = alloc() + val blockSize = alloc() + val result = VerQueryValueW(versionInfoPtr, versionId, block.ptr, blockSize.ptr) + + return if (result == 0) { + null + } else { + // Copy the bytes into a Kotlin byte array + val blockBytes = ByteArray(blockSize.value.convert()) + blockBytes.usePinned { pinned -> + memcpy(pinned.addressOf(0), block.value!!.reinterpret(), blockSize.value.convert()) + } + blockBytes.decodeToString() + } +} diff --git a/runtime/runtime-core/mingw/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderMingwTest.kt b/runtime/runtime-core/mingw/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderMingwTest.kt new file mode 100644 index 0000000000..22e95ef214 --- /dev/null +++ b/runtime/runtime-core/mingw/test/aws/smithy/kotlin/runtime/util/SystemPlatformProviderMingwTest.kt @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.util + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class SystemPlatformProviderMingwTest { + @Test + fun testOsInfo() = runTest { + val osInfo = PlatformProvider.System.osInfo() + println(osInfo) + assertEquals(OsFamily.Windows, osInfo.family) + assertNotNull(osInfo.version) + } +} 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..2d4c657af3 --- /dev/null +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipCompressor.kt @@ -0,0 +1,129 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.compression + +import aws.sdk.kotlin.crt.Closeable +import aws.smithy.kotlin.runtime.io.SdkBuffer +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 + */ +@OptIn(ExperimentalForeignApi::class) +internal class GzipCompressor : Closeable { + companion object { + internal const val BUFFER_SIZE = 16384 + } + + private val stream = nativeHeap.alloc() + private val outputBuffer = SdkBuffer() + internal var isClosed = false + + internal val availableForRead: Int + get() = outputBuffer.size.toInt() + + init { + // Initialize deflate with gzip encoding + val initResult = deflateInit2_( + stream.ptr, + Z_BEST_COMPRESSION, + Z_DEFLATED, + DEFAULT_WINDOW_BITS + WINDOW_BITS_GZIP_OFFSET, + MEM_LEVEL, + Z_DEFAULT_STRATEGY, + ZLIB_VERSION, + sizeOf().toInt(), + ) + + check(initResult == Z_OK) { "Failed to initialize zlib deflate with error code $initResult: ${zError(initResult)!!.toKString()}" } + } + + /** + * Update the compressor with [input] bytes + */ + 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 = compressionBuffer.pin() + stream.next_out = outputPin.addressOf(0).reinterpret() + stream.avail_out = BUFFER_SIZE.toUInt() + + val deflateResult = deflate(stream.ptr, Z_NO_FLUSH) + check(deflateResult == Z_OK) { "Deflate failed with error code $deflateResult" } + + val bytesWritten = BUFFER_SIZE - stream.avail_out.toInt() + outputBuffer.write(compressionBuffer, 0, bytesWritten) + + outputPin.unpin() + } + + inputPin.unpin() + } + + /** + * Consume [count] gzip-compressed bytes. + */ + 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 outputBuffer.readByteArray(count.toLong()) + } + + /** + * Flush the compressor and return the terminal sequence of bytes that represent the end of the gzip compression. + */ + fun flush(): ByteArray { + check(!isClosed) { "Compressor is closed" } + + memScoped { + val compressionBuffer = ByteArray(BUFFER_SIZE) + var deflateResult: Int? = null + var outputLength = 0L + + do { + val outputPin = compressionBuffer.pin() + stream.next_out = outputPin.addressOf(0).reinterpret() + stream.avail_out = BUFFER_SIZE.toUInt() + + 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.write(compressionBuffer, 0, bytesWritten) + + outputLength += bytesWritten.toLong() + outputPin.unpin() + } while (deflateResult != Z_STREAM_END) + + return outputBuffer.readByteArray(outputLength) + } + } + + 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 a9fc036399..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 @@ -5,6 +5,14 @@ package aws.smithy.kotlin.runtime.compression 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.buffer +import aws.smithy.kotlin.runtime.io.source +import aws.smithy.kotlin.runtime.io.use /** * The gzip compression algorithm. @@ -13,12 +21,31 @@ 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 bytes = stream.bytes() + if (bytes.isEmpty()) { + stream + } else { + GzipSdkSource(bytes.source()).use { + it.buffer().readByteArray().asByteStream() + } + } + } } } 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 new file mode 100644 index 0000000000..185019607e --- /dev/null +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/compression/GzipTestUtilsNative.kt @@ -0,0 +1,78 @@ +/* + * 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 kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pin +import kotlinx.cinterop.ptr +import kotlinx.cinterop.reinterpret +import kotlinx.cinterop.sizeOf +import kotlinx.cinterop.toKString +import platform.zlib.* + +/** + * Decompresses a byte array compressed using the gzip format + */ +@OptIn(ExperimentalForeignApi::class) +@InternalApi +public actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { + if (bytes.isEmpty()) { + return bytes + } + + val decompressedBuffer = UByteArray(bytes.size * 2).pin() + + memScoped { + val zStream = alloc() + + // 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 { + 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) { + zStream.next_out = decompressedBuffer.addressOf(0) + zStream.avail_out = decompressedBuffer.get().size.toUInt() + + val inflateResult = inflate(zStream.ptr, Z_NO_FLUSH) + + when (inflateResult) { + Z_OK, Z_STREAM_END -> { + val chunkSize = decompressedBuffer.get().size.toUInt() - zStream.avail_out + output.addAll(decompressedBuffer.get().copyOf(chunkSize.toInt())) + } + 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/content/BigDecimalNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigDecimalNative.kt index 202cebe004..b22085d029 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigDecimalNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigDecimalNative.kt @@ -4,46 +4,53 @@ */ package aws.smithy.kotlin.runtime.content -public actual class BigDecimal actual constructor(value: String) : +import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonSpinBigDecimal + +public actual class BigDecimal private constructor(private val delegate: IonSpinBigDecimal) : Number(), Comparable { - public actual fun toPlainString(): String { - TODO("Not yet implemented") - } - actual override fun toByte(): Byte { - TODO("Not yet implemented") + private companion object { + /** + * Returns a new or existing [BigDecimal] wrapper for the given delegate [value] + * @param value The delegate value to wrap + * @param left A candidate wrapper which may already contain [value] + * @param right A candidate wrapper which may already contain [value] + */ + fun coalesceOrCreate(value: IonSpinBigDecimal, left: BigDecimal, right: BigDecimal): BigDecimal = when (value) { + left.delegate -> left + right.delegate -> right + else -> BigDecimal(value) + } } - actual override fun toDouble(): Double { - TODO("Not yet implemented") - } + public actual constructor(value: String) : this(IonSpinBigDecimal.parseString(value, 10)) - actual override fun toFloat(): Float { - TODO("Not yet implemented") - } + public actual constructor(mantissa: BigInteger, exponent: Int) : + this(IonSpinBigDecimal.fromBigIntegerWithExponent(mantissa.delegate, exponent.toLong())) - actual override fun toInt(): Int { - TODO("Not yet implemented") - } - - actual override fun toLong(): Long { - TODO("Not yet implemented") - } - - actual override fun toShort(): Short { - TODO("Not yet implemented") - } + actual override fun toByte(): Byte = delegate.byteValue(exactRequired = false) + actual override fun toDouble(): Double = delegate.doubleValue(exactRequired = false) + actual override fun toFloat(): Float = delegate.floatValue(exactRequired = false) + actual override fun toInt(): Int = delegate.intValue(exactRequired = false) + actual override fun toLong(): Long = delegate.longValue(exactRequired = false) + actual override fun toShort(): Short = delegate.shortValue(exactRequired = false) public actual val mantissa: BigInteger - get() = TODO("Not yet implemented") + get() = BigInteger(delegate.significand) public actual val exponent: Int - get() = TODO("Not yet implemented") + get() = delegate.exponent.toInt() - public actual constructor(mantissa: BigInteger, exponent: Int) : this("TODO(Not yet implemented)") { - TODO("Not yet implemented") - } + actual override fun compareTo(other: BigDecimal): Int = delegate.compare(other.delegate) + actual override fun equals(other: Any?): Boolean = other is BigDecimal && other.delegate == delegate + actual override fun hashCode(): Int = 31 + delegate.hashCode() + public actual fun toPlainString(): String = delegate.toPlainString() + actual override fun toString(): String = delegate.toString() + + public actual operator fun plus(other: BigDecimal): BigDecimal = + coalesceOrCreate(delegate + other.delegate, this, other) - actual override fun compareTo(other: BigDecimal): Int = TODO("Not yet implemented") + public actual operator fun minus(other: BigDecimal): BigDecimal = + coalesceOrCreate(delegate - other.delegate, this, other) } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigIntegerNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigIntegerNative.kt index 5ddee8af74..09674a0d21 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigIntegerNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/content/BigIntegerNative.kt @@ -4,37 +4,48 @@ */ package aws.smithy.kotlin.runtime.content -public actual class BigInteger actual constructor(value: String) : +import com.ionspin.kotlin.bignum.integer.util.fromTwosComplementByteArray +import com.ionspin.kotlin.bignum.integer.util.toTwosComplementByteArray +import com.ionspin.kotlin.bignum.integer.BigInteger as IonSpinBigInteger + +public actual class BigInteger internal constructor(internal val delegate: IonSpinBigInteger) : Number(), Comparable { - public actual constructor(bytes: ByteArray) : this("Not yet implemented") - actual override fun toByte(): Byte { - TODO("Not yet implemented") + private companion object { + /** + * Returns a new or existing [BigInteger] wrapper for the given delegate [value] + * @param value The delegate value to wrap + * @param left A candidate wrapper which may already contain [value] + * @param right A candidate wrapper which may already contain [value] + */ + fun coalesceOrCreate(value: IonSpinBigInteger, left: BigInteger, right: BigInteger): BigInteger = when (value) { + left.delegate -> left + right.delegate -> right + else -> BigInteger(value) + } } - actual override fun toDouble(): Double { - TODO("Not yet implemented") - } + public actual constructor(value: String) : this(IonSpinBigInteger.parseString(value, 10)) + public actual constructor(bytes: ByteArray) : this(IonSpinBigInteger.fromTwosComplementByteArray(bytes)) - actual override fun toFloat(): Float { - TODO("Not yet implemented") - } + public actual override fun toByte(): Byte = delegate.byteValue(exactRequired = false) + public actual override fun toDouble(): Double = delegate.doubleValue(exactRequired = false) + public actual override fun toFloat(): Float = delegate.floatValue(exactRequired = false) + public actual override fun toInt(): Int = delegate.intValue(exactRequired = false) + public actual override fun toLong(): Long = delegate.longValue(exactRequired = false) + public actual override fun toShort(): Short = delegate.shortValue(exactRequired = false) - actual override fun toInt(): Int { - TODO("Not yet implemented") - } + public actual operator fun plus(other: BigInteger): BigInteger = + coalesceOrCreate(delegate + other.delegate, this, other) - actual override fun toLong(): Long { - TODO("Not yet implemented") - } - - actual override fun toShort(): Short { - TODO("Not yet implemented") - } + public actual operator fun minus(other: BigInteger): BigInteger = + coalesceOrCreate(delegate - other.delegate, this, other) - public actual operator fun plus(other: BigInteger): BigInteger = TODO("Not yet implemented") - public actual operator fun minus(other: BigInteger): BigInteger = TODO("Not yet implemented") - public actual fun toByteArray(): ByteArray = TODO("Not yet implemented") - actual override fun compareTo(other: BigInteger): Int = TODO("Not yet implemented") + public actual fun toByteArray(): ByteArray = delegate.toTwosComplementByteArray() + public actual override fun compareTo(other: BigInteger): Int = delegate.compare(other.delegate) + public actual override fun equals(other: Any?): Boolean = other is BigInteger && other.delegate == delegate + public actual override fun hashCode(): Int = 17 + delegate.hashCode() + public actual override fun toString(): String = toString(10) + public actual fun toString(radix: Int): String = delegate.toString(radix) } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Crc32Native.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Crc32Native.kt index 2c40576e88..ef86e4e519 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Crc32Native.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Crc32Native.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.hashing import aws.smithy.kotlin.runtime.InternalApi +import aws.sdk.kotlin.crt.util.hashing.Crc32 as CrtCrc32 /** * CRC-32 checksum. Note: [digest] will return the bytes (big endian) of the CRC32 integer value. Access [digestValue] @@ -12,15 +13,16 @@ import aws.smithy.kotlin.runtime.InternalApi */ @InternalApi public actual class Crc32 actual constructor() : Crc32Base() { - actual override fun digestValue(): UInt { - TODO("Not yet implemented") - } + private val delegate = CrtCrc32() - actual override fun update(input: ByteArray, offset: Int, length: Int) { - TODO("Not yet implemented") + actual override fun digestValue(): UInt { + val bytes = delegate.digest() + return ((bytes[0].toUInt() and 0xffu) shl 24) or + ((bytes[1].toUInt() and 0xffu) shl 16) or + ((bytes[2].toUInt() and 0xffu) shl 8) or + (bytes[3].toUInt() and 0xffu) } - actual override fun reset() { - TODO("Not yet implemented") - } + actual override fun update(input: ByteArray, offset: Int, length: Int): Unit = delegate.update(input, offset, length) + actual override fun reset(): Unit = delegate.reset() } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/EcdsaNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/EcdsaNative.kt index a2bca1c4e6..112b60df14 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/EcdsaNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/EcdsaNative.kt @@ -4,10 +4,7 @@ */ package aws.smithy.kotlin.runtime.hashing -// FIXME Implement using aws-c-cal: https://github.com/awslabs/aws-c-cal/blob/main/include/aws/cal/ecc.h -// Will need to be implemented and exposed in aws-crt-kotlin. Or maybe we can _only_ offer the CRT signer on Native? -// Will require updating DefaultAwsSigner to be expect/actual and set to CrtSigner on Native. /** * ECDSA on the SECP256R1 curve. */ -public actual fun ecdsaSecp256r1(key: ByteArray, message: ByteArray): ByteArray = TODO("Not yet implemented") +public actual fun ecdsaSecp256r1(key: ByteArray, message: ByteArray): ByteArray = error("This function should not be invoked on Native, which uses the CrtAwsSigner.") diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Md5Native.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Md5Native.kt index 9614543dad..efff134fc8 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Md5Native.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Md5Native.kt @@ -5,21 +5,15 @@ package aws.smithy.kotlin.runtime.hashing import aws.smithy.kotlin.runtime.InternalApi +import aws.sdk.kotlin.crt.util.hashing.Md5 as CrtMd5 /** * Implementation of RFC1321 MD5 digest */ @InternalApi public actual class Md5 actual constructor() : Md5Base() { - actual override fun update(input: ByteArray, offset: Int, length: Int) { - TODO("Not yet implemented") - } - - actual override fun digest(): ByteArray { - TODO("Not yet implemented") - } - - actual override fun reset() { - TODO("Not yet implemented") - } + private val delegate = CrtMd5() + actual override fun update(input: ByteArray, offset: Int, length: Int): Unit = delegate.update(input, offset, length) + actual override fun digest(): ByteArray = delegate.digest() + actual override fun reset(): Unit = delegate.reset() } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Sha1Native.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Sha1Native.kt index 6a4f4e1abc..541b25246f 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Sha1Native.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Sha1Native.kt @@ -5,21 +5,16 @@ package aws.smithy.kotlin.runtime.hashing import aws.smithy.kotlin.runtime.InternalApi +import aws.sdk.kotlin.crt.util.hashing.Sha1 as CrtSha1 /** * Implementation of SHA-1 (Secure Hash Algorithm 1) hash function. See: https://csrc.nist.gov/projects/hash-functions */ @InternalApi public actual class Sha1 actual constructor() : Sha1Base() { - actual override fun update(input: ByteArray, offset: Int, length: Int) { - TODO("Not yet implemented") - } + private val delegate = CrtSha1() - actual override fun digest(): ByteArray { - TODO("Not yet implemented") - } - - actual override fun reset() { - TODO("Not yet implemented") - } + actual override fun update(input: ByteArray, offset: Int, length: Int): Unit = delegate.update(input, offset, length) + actual override fun digest(): ByteArray = delegate.digest() + actual override fun reset(): Unit = delegate.reset() } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Sha256Native.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Sha256Native.kt index 6c85dfccea..69a82d4664 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Sha256Native.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/Sha256Native.kt @@ -5,10 +5,12 @@ package aws.smithy.kotlin.runtime.hashing import aws.smithy.kotlin.runtime.InternalApi +import aws.sdk.kotlin.crt.util.hashing.Sha256 as CrtSha256 @InternalApi public actual class Sha256 : Sha256Base() { - actual override fun update(input: ByteArray, offset: Int, length: Int): Unit = TODO("native not supported") - actual override fun digest(): ByteArray = TODO("native not supported") - actual override fun reset(): Unit = TODO("native not supported") + private val delegate = CrtSha256() + actual override fun update(input: ByteArray, offset: Int, length: Int): Unit = delegate.update(input, offset, length) + actual override fun digest(): ByteArray = delegate.digest() + actual override fun reset(): Unit = delegate.reset() } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapterNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapterNative.kt deleted file mode 100644 index 48f9badc3a..0000000000 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/BufferedSinkAdapterNative.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.smithy.kotlin.runtime.io - -internal actual class BufferedSinkAdapter actual constructor(sink: okio.BufferedSink) : SdkBufferedSink { - actual override val buffer: SdkBuffer - get() = TODO("Not yet implemented") - - actual override fun write(source: ByteArray, offset: Int, limit: Int) { - TODO("Not yet implemented") - } - - actual override fun writeAll(source: SdkSource): Long { - TODO("Not yet implemented") - } - - actual override fun write(source: SdkSource, byteCount: Long) { - TODO("Not yet implemented") - } - - actual override fun writeUtf8(string: String, start: Int, endExclusive: Int) { - TODO("Not yet implemented") - } - - actual override fun writeByte(x: Byte) { - TODO("Not yet implemented") - } - - actual override fun writeShort(x: Short) { - TODO("Not yet implemented") - } - - actual override fun writeShortLe(x: Short) { - TODO("Not yet implemented") - } - - actual override fun writeInt(x: Int) { - TODO("Not yet implemented") - } - - actual override fun writeIntLe(x: Int) { - TODO("Not yet implemented") - } - - actual override fun writeLong(x: Long) { - TODO("Not yet implemented") - } - - actual override fun writeLongLe(x: Long) { - TODO("Not yet implemented") - } - - actual override fun flush() { - TODO("Not yet implemented") - } - - actual override fun emit() { - TODO("Not yet implemented") - } - - actual override fun write(source: SdkBuffer, byteCount: Long) { - TODO("Not yet implemented") - } - - actual override fun close() { - TODO("Not yet implemented") - } -} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/BufferedSourceAdapterNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/BufferedSourceAdapterNative.kt deleted file mode 100644 index cfa9d4c76f..0000000000 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/BufferedSourceAdapterNative.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.smithy.kotlin.runtime.io - -internal actual class BufferedSourceAdapter actual constructor(source: okio.BufferedSource) : SdkBufferedSource { - actual override val buffer: SdkBuffer - get() = TODO("Not yet implemented") - - actual override fun skip(byteCount: Long) { - TODO("Not yet implemented") - } - - actual override fun readByte(): Byte { - TODO("Not yet implemented") - } - - actual override fun readShort(): Short { - TODO("Not yet implemented") - } - - actual override fun readShortLe(): Short { - TODO("Not yet implemented") - } - - actual override fun readLong(): Long { - TODO("Not yet implemented") - } - - actual override fun readLongLe(): Long { - TODO("Not yet implemented") - } - - actual override fun readInt(): Int { - TODO("Not yet implemented") - } - - actual override fun readIntLe(): Int { - TODO("Not yet implemented") - } - - actual override fun readAll(sink: SdkSink): Long { - TODO("Not yet implemented") - } - - actual override fun read(sink: ByteArray, offset: Int, limit: Int): Int { - TODO("Not yet implemented") - } - - actual override fun readByteArray(): ByteArray { - TODO("Not yet implemented") - } - - actual override fun readByteArray(byteCount: Long): ByteArray { - TODO("Not yet implemented") - } - - actual override fun readUtf8(): String { - TODO("Not yet implemented") - } - - actual override fun readUtf8(byteCount: Long): String { - TODO("Not yet implemented") - } - - actual override fun peek(): SdkBufferedSource { - TODO("Not yet implemented") - } - - actual override fun exhausted(): Boolean { - TODO("Not yet implemented") - } - - actual override fun request(byteCount: Long): Boolean { - TODO("Not yet implemented") - } - - actual override fun require(byteCount: Long) { - TODO("Not yet implemented") - } - - actual override fun read(sink: SdkBuffer, limit: Long): Long { - TODO("Not yet implemented") - } - - actual override fun close() { - TODO("Not yet implemented") - } -} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/ExceptionsNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/ExceptionsNative.kt index a5265fbb64..631e84c1bf 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/ExceptionsNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/ExceptionsNative.kt @@ -12,6 +12,7 @@ public actual open class IOException actual constructor( public actual constructor(message: String?) : this(message, null) } -public actual open class EOFException actual constructor(message: String?) : IOException(message) { - public actual constructor() : this(null) +public actual open class EOFException actual constructor(message: String?, cause: Throwable?) : IOException(message, cause) { + public actual constructor() : this(null, null) + public actual constructor(message: String?) : this(message, null) } 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..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 @@ -5,26 +5,62 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.compression.GzipCompressor /** - * 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 { + private val compressor = GzipCompressor() + actual override val availableForRead: Int - get() = TODO("Not yet implemented") + get() = compressor.availableForRead + actual override val isClosedForRead: Boolean - get() = TODO("Not yet implemented") + get() = channel.isClosedForRead && compressor.isClosed + 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 + 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()) + + if (rc > 0) { + val input = temp.readByteArray(rc) + compressor.update(input) + } + } + + // 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.flush() + sink.write(terminationBytes) + return terminationBytes.size.toLong().also { + compressor.close() + } + } + + // 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 { - TODO("Not yet implemented") + 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 f38cfd45d1..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 @@ -5,17 +5,50 @@ package aws.smithy.kotlin.runtime.io import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.compression.GzipCompressor /** - * 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 { + private val compressor = GzipCompressor() + actual override fun read(sink: SdkBuffer, limit: Long): Long { - TODO("Not yet implemented") + 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()) + + if (rc > 0) { + val input = temp.readByteArray(rc) + 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() + sink.write(terminationBytes) + return terminationBytes.size.toLong().also { + compressor.close() + } + } + + // 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() { - TODO("Not yet implemented") + compressor.close() + source.close() } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkBufferNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkBufferNative.kt deleted file mode 100644 index e8bee2b911..0000000000 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkBufferNative.kt +++ /dev/null @@ -1,160 +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 okio.Buffer - -public actual class SdkBuffer : - SdkBufferedSource, - SdkBufferedSink { - public actual val size: Long - get() = TODO("Not yet implemented") - - public actual constructor() - - internal actual val inner: okio.Buffer - get() = TODO("Not yet implemented") - - internal actual constructor(buffer: okio.Buffer) - - actual override val buffer: SdkBuffer - get() = TODO("Not yet implemented") - - actual override fun write(source: ByteArray, offset: Int, limit: Int) { - TODO("Not yet implemented") - } - - actual override fun writeAll(source: SdkSource): Long { - TODO("Not yet implemented") - } - - actual override fun write(source: SdkSource, byteCount: Long) { - TODO("Not yet implemented") - } - - actual override fun writeUtf8(string: String, start: Int, endExclusive: Int) { - TODO("Not yet implemented") - } - - actual override fun writeByte(x: Byte) { - TODO("Not yet implemented") - } - - actual override fun writeShort(x: Short) { - TODO("Not yet implemented") - } - - actual override fun writeShortLe(x: Short) { - TODO("Not yet implemented") - } - - actual override fun writeInt(x: Int) { - TODO("Not yet implemented") - } - - actual override fun writeIntLe(x: Int) { - TODO("Not yet implemented") - } - - actual override fun writeLong(x: Long) { - TODO("Not yet implemented") - } - - actual override fun writeLongLe(x: Long) { - TODO("Not yet implemented") - } - - actual override fun flush() { - TODO("Not yet implemented") - } - - actual override fun emit() { - TODO("Not yet implemented") - } - - actual override fun skip(byteCount: Long) { - TODO("Not yet implemented") - } - - actual override fun readByte(): Byte { - TODO("Not yet implemented") - } - - actual override fun readShort(): Short { - TODO("Not yet implemented") - } - - actual override fun readShortLe(): Short { - TODO("Not yet implemented") - } - - actual override fun readLong(): Long { - TODO("Not yet implemented") - } - - actual override fun readLongLe(): Long { - TODO("Not yet implemented") - } - - actual override fun readInt(): Int { - TODO("Not yet implemented") - } - - actual override fun readIntLe(): Int { - TODO("Not yet implemented") - } - - actual override fun readAll(sink: SdkSink): Long { - TODO("Not yet implemented") - } - - actual override fun read(sink: ByteArray, offset: Int, limit: Int): Int { - TODO("Not yet implemented") - } - - actual override fun readByteArray(): ByteArray { - TODO("Not yet implemented") - } - - actual override fun readByteArray(byteCount: Long): ByteArray { - TODO("Not yet implemented") - } - - actual override fun readUtf8(): String { - TODO("Not yet implemented") - } - - actual override fun readUtf8(byteCount: Long): String { - TODO("Not yet implemented") - } - - actual override fun peek(): SdkBufferedSource { - TODO("Not yet implemented") - } - - actual override fun exhausted(): Boolean { - TODO("Not yet implemented") - } - - actual override fun request(byteCount: Long): Boolean { - TODO("Not yet implemented") - } - - actual override fun require(byteCount: Long) { - TODO("Not yet implemented") - } - - actual override fun write(source: SdkBuffer, byteCount: Long) { - TODO("Not yet implemented") - } - - actual override fun read(sink: SdkBuffer, limit: Long): Long { - TODO("Not yet implemented") - } - - actual override fun close() { - TODO("Not yet implemented") - } -} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkSourceNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkSourceNative.kt deleted file mode 100644 index e7febda6d2..0000000000 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/SdkSourceNative.kt +++ /dev/null @@ -1,27 +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 aws.smithy.kotlin.runtime.InternalApi -import kotlinx.coroutines.CoroutineScope - -/** - * Consume the [SdkSource] and pull the entire contents into memory as a [ByteArray]. - */ -@InternalApi -public actual suspend fun SdkSource.readToByteArray(): ByteArray { - TODO("Not yet implemented") -} - -/** - * Convert the [SdkSource] to an [SdkByteReadChannel]. Content is read from the source and forwarded - * to the channel. - * @param coroutineScope the coroutine scope to use to launch a background reader channel responsible for propagating data - * between source and the returned channel - */ -@InternalApi -public actual fun SdkSource.toSdkByteReadChannel(coroutineScope: CoroutineScope?): SdkByteReadChannel { - TODO("Not yet implemented") -} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/internal/SdkDispatchersNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/internal/SdkDispatchersNative.kt index e5b1309c5a..de34d71300 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/internal/SdkDispatchersNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/io/internal/SdkDispatchersNative.kt @@ -6,6 +6,8 @@ package aws.smithy.kotlin.runtime.io.internal import aws.smithy.kotlin.runtime.InternalApi import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO @InternalApi public actual object SdkDispatchers { @@ -13,5 +15,5 @@ public actual object SdkDispatchers { * The CoroutineDispatcher that is designed for offloading blocking IO tasks to a shared pool of threads. */ public actual val IO: CoroutineDispatcher - get() = TODO("Not yet implemented") + get() = Dispatchers.IO } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt deleted file mode 100644 index 4b8fd7e7ae..0000000000 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.smithy.kotlin.runtime.net - -import aws.smithy.kotlin.runtime.InternalApi - -internal actual object DefaultHostResolver : HostResolver { - actual override suspend fun resolve(hostname: String): List { - TODO("Not yet implemented") - } - - actual override fun reportFailure(addr: HostAddress) { - TODO("Not yet implemented") - } - - @InternalApi - actual override fun purgeCache(addr: HostAddress?) { - TODO("Not yet implemented") - } -} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/time/DateTimeFormats.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/time/DateTimeFormats.kt new file mode 100644 index 0000000000..550a95cfe1 --- /dev/null +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/time/DateTimeFormats.kt @@ -0,0 +1,145 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.time + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.UtcOffset +import kotlinx.datetime.format.DateTimeComponents +import kotlinx.datetime.format.DateTimeFormat +import kotlinx.datetime.format.DayOfWeekNames +import kotlinx.datetime.format.MonthNames +import kotlinx.datetime.format.Padding +import kotlinx.datetime.format.alternativeParsing +import kotlinx.datetime.format.char +import kotlinx.datetime.format.optional + +/** + * [DateTimeFormat] for use with [kotlinx.datetime.Instant] + */ +internal object DateTimeFormats { + + /** + * ISO8601, full precision. Corresponds to [TimestampFormat.ISO_8601_FULL]. Truncate to microseconds for [TimestampFormat.ISO_8601]. + * e.g. "2020-11-05T19:22:37+00:00" + */ + val ISO_8601 = DateTimeComponents.Format { + // Two possible date formats: YYYY-MM-DD or YYYYMMDD + alternativeParsing({ + date( + LocalDate.Format { + year() + monthNumber() + dayOfMonth() + }, + ) + }) { + date( + LocalDate.Format { + year() + char('-') + monthNumber() + char('-') + dayOfMonth() + }, + ) + } + + char('T') + + // Two possible time formats: HH:MM:SS or HHMMSS + alternativeParsing({ + hour() + minute() + second() + }) { + hour() + char(':') + minute() + char(':') + second() + } + + // Fractional seconds + optional { + char('.') + secondFraction(1, 9) + } + + // Offsets + alternativeParsing({ + offsetHours() + }) { + offset(UtcOffset.Formats.ISO) + } + } + + /** + * ISO8601 condensed. Corresponds to [TimestampFormat.ISO_8601_CONDENSED]. + */ + val ISO_8601_CONDENSED = DateTimeComponents.Format { + year() + monthNumber() + dayOfMonth() + + char('T') + hour() + minute() + second() + char('Z') + } + + /** + * ISO8601 condensed, date only. Corresponds to [TimestampFormat.ISO_8601_CONDENSED_DATE] + */ + val ISO_8601_CONDENSED_DATE = DateTimeComponents.Format { + year() + monthNumber() + dayOfMonth() + } + + /** + * [RFC-5322/2822/822 IMF timestamp](https://tools.ietf.org/html/rfc5322). Corresponds to [TimestampFormat.RFC_5322]. + * e.g. "Thu, 05 Nov 2020 19:22:37 +0000" + */ + val RFC_5322 = DateTimeComponents.Format { + dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED) + chars(", ") + + alternativeParsing({ + dayOfMonth(padding = Padding.NONE) + }) { + dayOfMonth() + } + + char(' ') + monthName(MonthNames.ENGLISH_ABBREVIATED) + char(' ') + year() + char(' ') + + hour() + char(':') + minute() + char(':') + second() + char(' ') + + optional("GMT") { + offset(UtcOffset.Formats.FOUR_DIGITS) + } + } +} + +/** + * ISO8601 condensed, date only. Corresponds to [TimestampFormat.ISO_8601_CONDENSED_DATE]. + * Used only for _parsing_ ISO8601 condensed strings. Instant.parse() will fail if using [DateTimeFormats.ISO_8601_CONDENSED_DATE] + * because that is a [DateTimeFormat] which requires a time component to be present. + */ +internal val ISO_8601_CONDENSED_DATE_LOCALDATE = LocalDate.Format { + year() + monthNumber() + dayOfMonth() +} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/time/InstantNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/time/InstantNative.kt index 00bc92f2fa..01edcaf1af 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/time/InstantNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/time/InstantNative.kt @@ -5,91 +5,124 @@ package aws.smithy.kotlin.runtime.time +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import kotlinx.datetime.format import kotlin.time.Duration +import kotlinx.datetime.Instant as KtInstant -public actual class Instant : Comparable { - actual override fun compareTo(other: Instant): Int { - TODO("Not yet implemented") - } +private fun TimestampFormat.asDateTimeFormat() = when (this) { + TimestampFormat.RFC_5322 -> DateTimeFormats.RFC_5322 + TimestampFormat.ISO_8601_FULL -> DateTimeFormats.ISO_8601 + TimestampFormat.ISO_8601_CONDENSED -> DateTimeFormats.ISO_8601_CONDENSED + TimestampFormat.ISO_8601_CONDENSED_DATE -> DateTimeFormats.ISO_8601_CONDENSED_DATE + else -> throw IllegalArgumentException("TimestampFormat $this could not be converted to a DateTimeFormat") +} + +private fun KtInstant.truncateToMicros(): KtInstant = KtInstant.fromEpochSeconds(epochSeconds, nanosecondsOfSecond / 1_000 * 1_000) + +public actual class Instant(internal val delegate: KtInstant) : Comparable { - public actual val epochSeconds: Long - get() = TODO("Not yet implemented") - public actual val nanosecondsOfSecond: Int - get() = TODO("Not yet implemented") + actual override fun compareTo(other: Instant): Int = delegate.compareTo(other.delegate) + + public actual val epochSeconds: Long = delegate.epochSeconds + public actual val nanosecondsOfSecond: Int = delegate.nanosecondsOfSecond /** * Encode the [Instant] as a string into the format specified by [TimestampFormat] */ - public actual fun format(fmt: TimestampFormat): String { - TODO("Not yet implemented") + public actual fun format(fmt: TimestampFormat): String = when (fmt) { + TimestampFormat.ISO_8601 -> delegate.truncateToMicros().format(DateTimeFormats.ISO_8601) + TimestampFormat.EPOCH_SECONDS -> { + val s = delegate.epochSeconds.toString() + val ns = if (delegate.nanosecondsOfSecond != 0) { + ".${delegate.nanosecondsOfSecond.toString().padStart(9, '0').trimEnd('0')}" + } else { + "" + } + s + ns + } + else -> delegate.format(fmt.asDateTimeFormat()) } /** * Returns an instant that is the result of adding the specified [duration] to this instant. * NOTE: Duration may be negative in which case the returned Instant will be earlier than this Instant. */ - public actual operator fun plus(duration: Duration): Instant { - TODO("Not yet implemented") - } + public actual operator fun plus(duration: Duration): Instant = Instant(delegate + duration) /** * Returns an instant that is the result of subtracting the specified [duration] from this instant. * NOTE: Duration may be negative in which case the returned Instant will be later than this Instant. */ - public actual operator fun minus(duration: Duration): Instant { - TODO("Not yet implemented") - } + public actual operator fun minus(duration: Duration): Instant = Instant(delegate - duration) - public actual operator fun minus(other: Instant): Duration { - TODO("Not yet implemented") - } + public actual operator fun minus(other: Instant): Duration = delegate - other.delegate public actual companion object { /** * Parse an ISO-8601 formatted string into an [Instant] */ public actual fun fromIso8601(ts: String): Instant { - TODO("Not yet implemented") + val parseException = ParseException(ts, "Failed to parse $ts into an ISO-8601 timestamp", 0) + + listOf( + { DateTimeFormats.ISO_8601.parse(ts).apply { if (second == 60) second = 59 }.toInstantUsingOffset() }, + { KtInstant.parse(ts, DateTimeFormats.ISO_8601_CONDENSED) }, + { LocalDate.parse(ts, ISO_8601_CONDENSED_DATE_LOCALDATE).atStartOfDayIn(TimeZone.UTC) }, + ).forEach { parseFn -> + try { + return Instant(parseFn()) + } catch (e: IllegalArgumentException) { + parseException.addSuppressed(e) + } + } + + throw parseException } /** * Parse an RFC5322/RFC-822 formatted string into an [Instant] */ - public actual fun fromRfc5322(ts: String): Instant { - TODO("Not yet implemented") + public actual fun fromRfc5322(ts: String): Instant = try { + Instant(KtInstant.parse(ts, DateTimeFormats.RFC_5322)) + } catch (e: IllegalArgumentException) { + throw ParseException(ts, "Failed to parse $ts into an RFC-5322 timestamp", 0) } /** * Create an [Instant] from its parts */ - public actual fun fromEpochSeconds(seconds: Long, ns: Int): Instant { - TODO("Not yet implemented") + public actual fun fromEpochSeconds(seconds: Long, ns: Int): Instant = try { + Instant(KtInstant.fromEpochSeconds(seconds, ns)) + } catch (e: IllegalArgumentException) { + throw ParseException("${seconds}s, ${ns}ns", "Failed to parse (${seconds}s, ${ns}ns) into an epoch seconds timestamp", 0) } /** * Parse a string formatted as epoch-seconds into an [Instant] */ - public actual fun fromEpochSeconds(ts: String): Instant { - TODO("Not yet implemented") - } + public actual fun fromEpochSeconds(ts: String): Instant = fromEpochSeconds(ts.toLong(), 0) /** * Create an [Instant] from the current system time */ - public actual fun now(): Instant { - TODO("Not yet implemented") - } + public actual fun now(): Instant = Instant(Clock.System.now()) /** * Create an [Instant] with the minimum possible value */ - public actual val MIN_VALUE: Instant - get() = TODO("Not yet implemented") + public actual val MIN_VALUE: Instant = Instant(KtInstant.DISTANT_PAST) /** * Create an [Instant] with the maximum possible value */ - public actual val MAX_VALUE: Instant - get() = TODO("Not yet implemented") + public actual val MAX_VALUE: Instant = Instant(KtInstant.DISTANT_FUTURE) } + + public override fun equals(other: Any?): Boolean = other is Instant && delegate == other.delegate + public override fun toString(): String = delegate.toString() + public override fun hashCode(): Int = delegate.hashCode() } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt deleted file mode 100644 index bb435ccaea..0000000000 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/PlatformNative.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.smithy.kotlin.runtime.util - -internal actual object SystemDefaultProvider : PlatformProvider { - actual override fun getAllEnvVars(): Map { - TODO("Not yet implemented") - } - - actual override fun getenv(key: String): String? { - TODO("Not yet implemented") - } - - actual override val filePathSeparator: String - get() = TODO("Not yet implemented") - - actual override suspend fun readFileOrNull(path: String): ByteArray? { - TODO("Not yet implemented") - } - - actual override suspend fun writeFile(path: String, data: ByteArray) { - TODO("Not yet implemented") - } - - actual override fun fileExists(path: String): Boolean { - TODO("Not yet implemented") - } - - actual override fun osInfo(): OperatingSystem { - TODO("Not yet implemented") - } - - actual override val isJvm: Boolean - get() = TODO("Not yet implemented") - actual override val isAndroid: Boolean - get() = TODO("Not yet implemented") - actual override val isBrowser: Boolean - get() = TODO("Not yet implemented") - actual override val isNode: Boolean - get() = TODO("Not yet implemented") - actual override val isNative: Boolean - get() = TODO("Not yet implemented") - - actual override fun getAllProperties(): Map { - TODO("Not yet implemented") - } - - actual override fun getProperty(key: String): String? { - TODO("Not yet implemented") - } -} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/SystemDefaultProviderBase.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/SystemDefaultProviderBase.kt new file mode 100644 index 0000000000..fde52492e8 --- /dev/null +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/util/SystemDefaultProviderBase.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.util + +import aws.smithy.kotlin.runtime.io.IOException +import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers +import kotlinx.cinterop.* +import kotlinx.coroutines.withContext +import platform.posix.* + +@OptIn(ExperimentalForeignApi::class) +public abstract class SystemDefaultProviderBase : PlatformProvider { + override fun getenv(key: String): String? = platform.posix.getenv(key)?.toKString() + + override suspend fun readFileOrNull(path: String): ByteArray? = withContext(SdkDispatchers.IO) { + try { + val size = memScoped { + val statResult = alloc() + if (stat(path, statResult.ptr) != 0) return@withContext null + statResult.st_size.convert() + } + + val file = fopen(path, "rb") ?: return@withContext null + + try { + // Read file content + val buffer = ByteArray(size).pin() + val rc = fread(buffer.addressOf(0), 1uL, size.toULong(), file) + if (rc == size.toULong()) buffer.get() else null + } finally { + fclose(file) + } + } catch (_: Exception) { + null + } + } + + override suspend fun writeFile(path: String, data: ByteArray): Unit = withContext(SdkDispatchers.IO) { + val file = fopen(path, "wb") ?: throw IOException("Cannot open file for writing: $path") + try { + val wc = fwrite(data.refTo(0), 1uL, data.size.toULong(), file) + if (wc != data.size.toULong()) { + throw IOException("Failed to write all bytes to file $path, expected ${data.size.toLong()}, wrote $wc") + } + } finally { + fclose(file) + } + } + + override fun fileExists(path: String): Boolean = access(path, F_OK) == 0 + + override val isJvm: Boolean = false + override val isAndroid: Boolean = false + override val isBrowser: Boolean = false + override val isNode: Boolean = false + override val isNative: Boolean = true + + // Kotlin/Native doesn't have system properties + override fun getAllProperties(): Map = emptyMap() + override fun getProperty(key: String): String? = null +} 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 deleted file mode 100644 index 198808f770..0000000000 --- a/runtime/runtime-core/native/test/aws/smithy/kotlin/runtime/io/GzipTestUtilsNative.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.smithy.kotlin.runtime.io - -/** - * Decompresses a byte array compressed using the gzip format - */ -internal actual fun decompressGzipBytes(bytes: ByteArray): ByteArray { - TODO("Not yet implemented") -} diff --git a/runtime/runtime-core/posix/src/aws/smithy/kotlin/runtime/net/DefaultHostResolverPosix.kt b/runtime/runtime-core/posix/src/aws/smithy/kotlin/runtime/net/DefaultHostResolverPosix.kt new file mode 100644 index 0000000000..b871ea5095 --- /dev/null +++ b/runtime/runtime-core/posix/src/aws/smithy/kotlin/runtime/net/DefaultHostResolverPosix.kt @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.net + +import kotlinx.cinterop.* +import platform.posix.* + +@OptIn(ExperimentalForeignApi::class) +internal actual object DefaultHostResolver : HostResolver { + actual override suspend fun resolve(hostname: String): List = memScoped { + val hints = alloc().apply { + ai_family = AF_UNSPEC // Allow both IPv4 and IPv6 + ai_socktype = SOCK_STREAM // TCP stream sockets + ai_flags = AI_PASSIVE // For wildcard IP address + } + + val result = allocPointerTo() + + try { + // Perform the DNS lookup + val status = getaddrinfo(hostname, null, hints.ptr, result.ptr) + check(status == 0) { "Failed to resolve host $hostname: ${gai_strerror(status)?.toKString()}" } + + return generateSequence(result.value) { it.pointed.ai_next } + .map { it.pointed.ai_addr!!.pointed.toIpAddr() } + .map { HostAddress(hostname, it) } + .toList() + } finally { + freeaddrinfo(result.value) + } + } + + private fun sockaddr.toIpAddr(): IpAddr { + val (size, addrPtr, constructor) = when (sa_family.toInt()) { + AF_INET -> Triple( + 4, + reinterpret().sin_addr.ptr, + { bytes: ByteArray -> IpV4Addr(bytes) }, + ) + AF_INET6 -> Triple( + 16, + reinterpret().sin6_addr.ptr, + { bytes: ByteArray -> IpV6Addr(bytes) }, + ) + else -> throw IllegalArgumentException("Unsupported sockaddr family $sa_family") + } + + val ipBytes = ByteArray(size) + memcpy(ipBytes.refTo(0), addrPtr, size.toULong()) + return constructor(ipBytes) + } + + actual override fun reportFailure(addr: HostAddress) { + // No-op, same as JVM implementation + } + + actual override fun purgeCache(addr: HostAddress?) { + // No-op, same as JVM implementation + } +} diff --git a/runtime/runtime-core/posix/src/aws/smithy/kotlin/runtime/util/SystemDefaultProviderPosix.kt b/runtime/runtime-core/posix/src/aws/smithy/kotlin/runtime/util/SystemDefaultProviderPosix.kt new file mode 100644 index 0000000000..9c1010c6b0 --- /dev/null +++ b/runtime/runtime-core/posix/src/aws/smithy/kotlin/runtime/util/SystemDefaultProviderPosix.kt @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.util + +import aws.smithy.platform.posix.get_environ_ptr +import kotlinx.cinterop.* +import platform.posix.uname +import platform.posix.utsname + +@OptIn(ExperimentalForeignApi::class) +public actual object SystemDefaultProvider : SystemDefaultProviderBase() { + actual override val filePathSeparator: String = "/" + + actual override fun osInfo(): OperatingSystem = memScoped { + val utsname = alloc() + uname(utsname.ptr) + + val sysName = utsname.sysname.toKString().lowercase() + val version = utsname.release.toKString() + val machine = utsname.machine.toKString().lowercase() // Helps differentiate Apple platforms + + val family = when { + sysName.contains("darwin") -> { + when { + machine.startsWith("iphone") -> OsFamily.Ios + // TODO Validate that iPadOS/tvOS/watchOS resolves correctly on each of these devices + machine.startsWith("ipad") -> OsFamily.IpadOs + machine.startsWith("tv") -> OsFamily.TvOs + machine.startsWith("watch") -> OsFamily.WatchOs + else -> OsFamily.MacOs + } + } + sysName.contains("linux") -> OsFamily.Linux + else -> OsFamily.Unknown + } + + return OperatingSystem(family, version) + } + + actual override fun getAllEnvVars(): Map = memScoped { + val environ = get_environ_ptr() + generateSequence(0) { it + 1 } + .map { idx -> environ?.get(idx)?.toKString() } + .takeWhile { it != null } + .associate { env -> + val parts = env?.split("=", limit = 2) + check(parts?.size == 2) { "Environment entry \"$env\" is malformed" } + parts[0] to parts[1] + } + } +} diff --git a/runtime/runtime-core/posix/src/posixInterop/cinterop/environ.h b/runtime/runtime-core/posix/src/posixInterop/cinterop/environ.h new file mode 100644 index 0000000000..aa6348ea0d --- /dev/null +++ b/runtime/runtime-core/posix/src/posixInterop/cinterop/environ.h @@ -0,0 +1,12 @@ +#ifndef ENVIRON_H +#define ENVIRON_H + +// External declaration to get environment variables +extern char **environ; + +// Helper function to get the environ pointer +char** get_environ_ptr() { + return environ; +} + +#endif \ No newline at end of file diff --git a/runtime/serde/serde-cbor/common/test/aws/smithy/kotlin/runtime/serde/cbor/CborSerializerTest.kt b/runtime/serde/serde-cbor/common/test/aws/smithy/kotlin/runtime/serde/cbor/CborSerializerTest.kt index 918fe764ba..63da8441e5 100644 --- a/runtime/serde/serde-cbor/common/test/aws/smithy/kotlin/runtime/serde/cbor/CborSerializerTest.kt +++ b/runtime/serde/serde-cbor/common/test/aws/smithy/kotlin/runtime/serde/cbor/CborSerializerTest.kt @@ -290,12 +290,10 @@ class CborSerializerTest { val buffer = SdkBuffer().apply { write(serializer.toByteArray()) } val deserializer = CborPrimitiveDeserializer(buffer) - tests.dropLast(1).forEach { + tests.forEach { assertEquals(it.epochMilliseconds, deserializer.deserializeInstant(TimestampFormat.EPOCH_SECONDS).epochMilliseconds) } - // FIXME Serializing -> deserializing Instant.MAX_VALUE results in a one millisecond offset... - assertEquals(Instant.MAX_VALUE.epochMilliseconds, deserializer.deserializeInstant(TimestampFormat.EPOCH_SECONDS).epochMilliseconds - 1) assertEquals(0, buffer.size) } diff --git a/settings.gradle.kts b/settings.gradle.kts index dcd6c01bdb..a39f42c908 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -83,6 +83,7 @@ include(":runtime:auth:http-auth-api") include(":runtime:auth:http-auth-aws") include(":runtime:auth:identity-api") include(":runtime:crt-util") +include(":runtime:observability:logging-crt") include(":runtime:observability:logging-slf4j2") include(":runtime:observability:telemetry-api") include(":runtime:observability:telemetry-defaults") diff --git a/tests/codegen/waiter-tests/build.gradle.kts b/tests/codegen/waiter-tests/build.gradle.kts index 34f5cc852e..4171d35fee 100644 --- a/tests/codegen/waiter-tests/build.gradle.kts +++ b/tests/codegen/waiter-tests/build.gradle.kts @@ -38,6 +38,7 @@ kotlin.sourceSets.getByName("main") { tasks.withType { dependsOn(tasks.generateSmithyProjections) + // FIXME Re-enable warnings as errors SDK-KT-785 compilerOptions { allWarningsAsErrors = false