├── .github └── workflows │ ├── android_integration.yaml │ ├── merged.yaml │ ├── released.yaml │ └── review.yaml ├── .gitignore ├── .kotlin └── metadata │ └── kotlinTransformedMetadataLibraries │ ├── org.jetbrains.kotlin-kotlin-stdlib-2.0.0-commonMain-2bbUHA.klib │ ├── org.jetbrains.kotlin-kotlin-test-2.0.0-annotationsCommonMain-24eTFQ.klib │ ├── org.jetbrains.kotlin-kotlin-test-2.0.0-assertionsCommonMain-24eTFQ.klib │ ├── org.jetbrains.kotlinx-atomicfu-0.23.1-commonMain-wFq7cg.klib │ ├── org.jetbrains.kotlinx-atomicfu-0.23.1-nativeMain-wFq7cg.klib │ ├── org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-commonMain-XanZ2w.klib │ ├── org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-concurrentMain-XanZ2w.klib │ ├── org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-nativeDarwinMain-sy5nKg.klib │ ├── org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-nativeMain-XanZ2w.klib │ └── org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-nativeOtherMain-XanZ2w.klib ├── LICENSE.md ├── Readme.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── karma.config.d └── karma.conf.js ├── kotlin-js-store └── yarn.lock ├── settings.gradle.kts ├── src ├── androidInstrumentedTest │ ├── AndroidManifest.xml │ ├── aidl │ │ └── com │ │ │ └── ditchoom │ │ │ └── buffer │ │ │ └── BufferIpcTestAidl.aidl │ └── kotlin │ │ └── com │ │ └── ditchoom │ │ └── buffer │ │ ├── IPCSimpleService.kt │ │ ├── IPCTest.kt │ │ └── ParcelableTests.kt ├── androidMain │ ├── AndroidManifest.xml │ ├── aidl │ │ └── com │ │ │ └── ditchoom │ │ │ └── buffer │ │ │ └── JvmBuffer.aidl │ └── kotlin │ │ └── com │ │ └── ditchoom │ │ └── buffer │ │ ├── BaseJvmBuffer.kt │ │ ├── BufferFactory.kt │ │ ├── CharsetEncoderHelper.kt │ │ ├── JvmBuffer.kt │ │ ├── Parcelable.kt │ │ └── ParcelableSharedMemoryBuffer.kt ├── appleMain │ └── kotlin │ │ └── com │ │ └── ditchoom │ │ └── buffer │ │ ├── BufferFactory.kt │ │ ├── CharsetEncoderHelper.kt │ │ ├── DataBuffer.kt │ │ └── MutableDataBuffer.kt ├── commonMain │ └── kotlin │ │ └── com │ │ └── ditchoom │ │ └── buffer │ │ ├── AllocationZone.kt │ │ ├── BufferFactory.kt │ │ ├── ByteOrder.kt │ │ ├── Charset.kt │ │ ├── FragmentedReadBuffer.kt │ │ ├── Parcelable.kt │ │ ├── PlatformBuffer.kt │ │ ├── PositionBuffer.kt │ │ ├── ReadBuffer.kt │ │ ├── SuspendCloseable.kt │ │ ├── TransformedReadBuffer.kt │ │ └── WriteBuffer.kt ├── commonTest │ └── kotlin │ │ └── com │ │ └── ditchoom │ │ └── buffer │ │ ├── BufferTests.kt │ │ ├── FragmentedReadBufferTests.kt │ │ └── TransformedReadBufferTest.kt ├── jsMain │ └── kotlin │ │ └── com │ │ └── ditchoom │ │ └── buffer │ │ ├── BufferFactory.kt │ │ ├── JsBuffer.kt │ │ └── Parcelable.kt ├── jvmMain │ └── kotlin │ │ └── com │ │ └── ditchoom │ │ └── buffer │ │ ├── BaseJvmBuffer.kt │ │ ├── BufferFactory.kt │ │ ├── CharsetEncoderHelper.kt │ │ ├── JvmBuffer.kt │ │ └── Parcelable.kt ├── linuxMain │ └── kotlin │ │ └── com │ │ └── ditchoom │ │ └── buffer │ │ └── BufferFactory.kt └── nativeMain │ └── kotlin │ └── buffer │ ├── NativeBuffer.kt │ └── Parcelable.kt └── webpack.config.d └── config.js /.github/workflows/android_integration.yaml: -------------------------------------------------------------------------------- 1 | name: "Android Emulator Integration Test" 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - '*.md' 6 | types: 7 | - synchronize 8 | - opened 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | api-level: [19, 29] 15 | steps: 16 | - name: checkout 17 | uses: actions/checkout@v4 18 | - name: Set up JDK 19 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'zulu' 22 | java-version: '19' 23 | cache: gradle 24 | - name: Enable KVM 25 | run: | 26 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 27 | sudo udevadm control --reload-rules 28 | sudo udevadm trigger --name-match=kvm 29 | 30 | - name: Gradle cache 31 | uses: gradle/actions/setup-gradle@v3 32 | 33 | - name: AVD cache 34 | uses: actions/cache@v4 35 | id: avd-cache 36 | with: 37 | path: | 38 | ~/.android/avd/* 39 | ~/.android/adb* 40 | key: avd-${{ matrix.api-level }} 41 | 42 | - name: create AVD and generate snapshot for caching 43 | if: steps.avd-cache.outputs.cache-hit != 'true' 44 | uses: reactivecircus/android-emulator-runner@v2 45 | with: 46 | api-level: ${{ matrix.api-level }} 47 | force-avd-creation: false 48 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 49 | disable-animations: false 50 | script: echo "Generated AVD snapshot for caching." 51 | 52 | - name: run tests 53 | uses: reactivecircus/android-emulator-runner@v2 54 | with: 55 | api-level: ${{ matrix.api-level }} 56 | force-avd-creation: false 57 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 58 | disable-animations: true 59 | script: ./gradlew connectedCheck -------------------------------------------------------------------------------- /.github/workflows/merged.yaml: -------------------------------------------------------------------------------- 1 | name: "Build Test and Deploy" 2 | on: 3 | pull_request_target: 4 | paths-ignore: 5 | - '*.md' 6 | branches: [ main ] 7 | types: 8 | - closed 9 | env: 10 | username: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 11 | password: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 12 | jobs: 13 | create-staging-repository: 14 | if: github.event.pull_request.merged == true 15 | name: Create staging repository 16 | runs-on: ubuntu-latest 17 | outputs: 18 | repository_id: ${{ steps.create.outputs.repository_id }} 19 | steps: 20 | - id: create 21 | run: | 22 | jsonOutput=$(curl -s --request POST -u "$username:$password" \ 23 | --url https://oss.sonatype.org/service/local/staging/profiles/3abaab4608e7e/start \ 24 | --header 'Accept: application/json' \ 25 | --header 'Content-Type: application/json' \ 26 | --data '{ "data": {"description" : "Buffer"} }') 27 | stagingRepositoryId=$(echo "$jsonOutput" | jq -r '.data.stagedRepositoryId') 28 | if [ -z "$stagingRepositoryId" ]; then 29 | echo "Error while creating the staging repository." 30 | exit 1 31 | else 32 | echo "repository_id=$stagingRepositoryId" >> $GITHUB_OUTPUT 33 | fi 34 | macos: 35 | name: MacOS Build & Upload artifacts to sonatype 36 | if: github.event.pull_request.merged == true 37 | needs: create-staging-repository 38 | runs-on: macOS-latest 39 | env: 40 | SONATYPE_REPOSITORY_ID: ${{ needs.create-staging-repository.outputs.repository_id }} 41 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 42 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 43 | GPG_SECRET: ${{ secrets.GPG_SECRET }} 44 | GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} 45 | NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 46 | steps: 47 | - uses: actions/checkout@v4 48 | - name: Set up JDK 19 49 | uses: actions/setup-java@v4 50 | with: 51 | distribution: 'zulu' 52 | java-version: '19' 53 | cache: gradle 54 | - name: Gradle cache 55 | uses: gradle/actions/setup-gradle@v3 56 | - name: Setup Chrome 57 | uses: browser-actions/setup-chrome@v1 58 | - name: Setup Android SDK 59 | uses: android-actions/setup-android@v2 60 | - name: Import GPG Key 61 | id: import_gpg 62 | uses: crazy-max/ghaction-import-gpg@v5 63 | with: 64 | gpg_private_key: ${{ secrets.GPG_SECRET }} 65 | passphrase: ${{ secrets.GPG_SIGNING_PASSWORD }} 66 | - name: Test and deploy with Gradle Major 67 | if: ${{ contains(github.event.pull_request.labels.*.name, 'major') }} 68 | env: 69 | SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 70 | SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 71 | GPG_SECRET: ${{ secrets.GPG_SECRET }} 72 | GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} 73 | NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 74 | run: ./gradlew publishAllPublicationsToSonatypeRepository -PincrementMajor=true 75 | - name: Test and deploy with Gradle Minor 76 | if: ${{ contains(github.event.pull_request.labels.*.name, 'minor') }} 77 | env: 78 | SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 79 | SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 80 | GPG_SECRET: ${{ secrets.GPG_SECRET }} 81 | GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} 82 | NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 83 | run: ./gradlew publishAllPublicationsToSonatypeRepository -PincrementMinor=true 84 | - name: Test and deploy with Gradle Patch 85 | if: ${{ !contains(github.event.pull_request.labels.*.name, 'major') && !contains(github.event.pull_request.labels.*.name, 'minor') }} 86 | env: 87 | SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 88 | SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 89 | GPG_SECRET: ${{ secrets.GPG_SECRET }} 90 | GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }} 91 | NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 92 | run: ./gradlew publishAllPublicationsToSonatypeRepository 93 | - uses: actions/upload-artifact@v3 94 | with: 95 | name: Package 96 | path: build/libs 97 | finalize: 98 | runs-on: ubuntu-latest 99 | needs: [create-staging-repository, macos] 100 | if: ${{ always() && needs.create-staging-repository.result == 'success' && github.event.pull_request.merged == true }} 101 | steps: 102 | - name: Discard 103 | if: ${{ needs.macos.result != 'success' }} 104 | uses: nexus-actions/drop-nexus-staging-repo@main 105 | with: 106 | username: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 107 | password: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 108 | staging_repository_id: ${{ needs.create-staging-repository.outputs.repository-id }} 109 | - name: Release 110 | if: ${{ needs.macos.result == 'success' }} 111 | uses: nexus-actions/release-nexus-staging-repo@main 112 | with: 113 | username: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 114 | password: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 115 | staging_repository_id: ${{ needs.create-staging-repository.outputs.repository_id }} -------------------------------------------------------------------------------- /.github/workflows/released.yaml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | name: Release pushed tag 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Create release 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | tag: ${{ github.ref_name }} 20 | run: | 21 | gh release create "$tag" \ 22 | --repo="$GITHUB_REPOSITORY" \ 23 | --title="${GITHUB_REPOSITORY#*/} ${tag#v}" \ 24 | --generate-notes -------------------------------------------------------------------------------- /.github/workflows/review.yaml: -------------------------------------------------------------------------------- 1 | name: "Build and Test" 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - '*.md' 6 | types: 7 | - synchronize 8 | - opened 9 | jobs: 10 | review: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ macos-latest, ubuntu-latest ] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up JDK 19 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'zulu' 21 | java-version: '19' 22 | cache: gradle 23 | - name: Setup Chrome 24 | uses: browser-actions/setup-chrome@v1 25 | - name: Gradle cache 26 | uses: gradle/actions/setup-gradle@v3 27 | - name: Tests with Gradle Major 28 | if: ${{ contains(github.event.pull_request.labels.*.name, 'major') }} 29 | run: ./gradlew ktlintCheck assemble build check allTests publishToMavenLocal -PincrementMajor=true 30 | - name: Tests with Gradle Minor 31 | if: ${{ contains(github.event.pull_request.labels.*.name, 'minor') }} 32 | run: ./gradlew ktlintCheck assemble build check allTests publishToMavenLocal -PincrementMinor=true 33 | - name: Tests with Gradle Patch 34 | if: ${{ !contains(github.event.pull_request.labels.*.name, 'major') && !contains(github.event.pull_request.labels.*.name, 'minor') }} 35 | run: ./gradlew ktlintCheck assemble build check allTests publishToMavenLocal 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | bin 3 | .idea 4 | *.iml 5 | .gradle 6 | *.swp 7 | .project 8 | 9 | kotlinx-io-native/repo/ 10 | 11 | !.idea/vcs.xml 12 | !.idea/runConfigurations 13 | !.idea/codeStyles 14 | 15 | local.properties -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-stdlib-2.0.0-commonMain-2bbUHA.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-stdlib-2.0.0-commonMain-2bbUHA.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-test-2.0.0-annotationsCommonMain-24eTFQ.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-test-2.0.0-annotationsCommonMain-24eTFQ.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-test-2.0.0-assertionsCommonMain-24eTFQ.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-test-2.0.0-assertionsCommonMain-24eTFQ.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-atomicfu-0.23.1-commonMain-wFq7cg.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-atomicfu-0.23.1-commonMain-wFq7cg.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-atomicfu-0.23.1-nativeMain-wFq7cg.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-atomicfu-0.23.1-nativeMain-wFq7cg.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-commonMain-XanZ2w.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-commonMain-XanZ2w.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-concurrentMain-XanZ2w.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-concurrentMain-XanZ2w.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-nativeDarwinMain-sy5nKg.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-nativeDarwinMain-sy5nKg.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-nativeMain-XanZ2w.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-nativeMain-XanZ2w.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-nativeOtherMain-XanZ2w.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlinx-kotlinx-coroutines-core-1.8.1-nativeOtherMain-XanZ2w.klib -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined 10 | by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is 13 | granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are 16 | controlled by, or are under common control with that entity. For the purposes of this definition, 17 | "control" means (i) the power, direct or indirect, to cause the direction or management of such 18 | entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 19 | outstanding shares, or (iii) 20 | beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this 23 | License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including but not limited 26 | to software source code, documentation source, and configuration files. 27 | 28 | "Object" form shall mean any form resulting from mechanical transformation or translation of a 29 | Source form, including but not limited to compiled object code, generated documentation, and 30 | conversions to other media types. 31 | 32 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under 33 | the License, as indicated by a copyright notice that is included in or attached to the work 34 | (an example is provided in the Appendix below). 35 | 36 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or 37 | derived from) the Work and for which the editorial revisions, annotations, elaborations, or other 38 | modifications represent, as a whole, an original work of authorship. For the purposes of this 39 | License, Derivative Works shall not include works that remain separable from, or merely link (or 40 | bind by name) to the interfaces of, the Work and Derivative Works thereof. 41 | 42 | "Contribution" shall mean any work of authorship, including the original version of the Work and 43 | any modifications or additions to that Work or Derivative Works thereof, that is intentionally 44 | submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or 45 | Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this 46 | definition, "submitted" 47 | means any form of electronic, verbal, or written communication sent to the Licensor or its 48 | representatives, including but not limited to communication on electronic mailing lists, source 49 | code control systems, and issue tracking systems that are managed by, or on behalf of, the 50 | Licensor for the purpose of discussing and improving the Work, but excluding communication that 51 | is conspicuously marked or otherwise designated in writing by the copyright owner as " 52 | Not a Contribution." 53 | 54 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a 55 | Contribution has been received by Licensor and subsequently incorporated within the Work. 56 | 57 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor 58 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable 59 | copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, 60 | sublicense, and distribute the Work and such Derivative Works in Source or Object form. 61 | 62 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor 63 | hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable 64 | (except as stated in this section) patent license to make, have made, use, offer to sell, sell, 65 | import, and otherwise transfer the Work, where such license applies only to those patent claims 66 | licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or 67 | by combination of their Contribution(s) 68 | with the Work to which such Contribution(s) was submitted. If You institute patent litigation 69 | against any entity ( 70 | including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution 71 | incorporated within the Work constitutes direct or contributory patent infringement, then any 72 | patent licenses granted to You under this License for that Work shall terminate as of the date 73 | such litigation is filed. 74 | 75 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof 76 | in any medium, with or without modifications, and in Source or Object form, provided that You 77 | meet the following conditions: 78 | 79 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; 80 | and 81 | 82 | (b) You must cause any modified files to carry prominent notices stating that You changed the 83 | files; and 84 | 85 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all 86 | copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding 87 | those notices that do not pertain to any part of the Derivative Works; and 88 | 89 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative 90 | Works that You distribute must include a readable copy of the attribution notices contained 91 | within such NOTICE file, excluding those notices that do not pertain to any part of the 92 | Derivative Works, in at least one of the following places: within a NOTICE text file distributed 93 | as part of the Derivative Works; within the Source form or documentation, if provided along with 94 | the Derivative Works; or, within a display generated by the Derivative Works, if and wherever 95 | such third-party notices normally appear. The contents of the NOTICE file are for informational 96 | purposes only and do not modify the License. You may add Your own attribution notices within 97 | Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the 98 | Work, provided that such additional attribution notices cannot be construed as modifying the 99 | License. 100 | 101 | You may add Your own copyright statement to Your modifications and may provide additional or 102 | different license terms and conditions for use, reproduction, or distribution of Your 103 | modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and 104 | distribution of the Work otherwise complies with the conditions stated in this License. 105 | 106 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution 107 | intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms 108 | and conditions of this License, without any additional terms or conditions. Notwithstanding the 109 | above, nothing herein shall supersede or modify the terms of any separate license agreement you 110 | may have executed with Licensor regarding such Contributions. 111 | 112 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service 113 | marks, or product names of the Licensor, except as required for reasonable and customary use in 114 | describing the origin of the Work and reproducing the content of the NOTICE file. 115 | 116 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor 117 | provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT 118 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, 119 | any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 120 | PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or 121 | redistributing the Work and assume any risks associated with Your exercise of permissions under 122 | this License. 123 | 124 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including 125 | negligence), contract, or otherwise, unless required by applicable law (such as deliberate and 126 | grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for 127 | damages, including any direct, indirect, special, incidental, or consequential damages of any 128 | character arising as a result of this License or out of the use or inability to use the Work ( 129 | including but not limited to damages for loss of goodwill, work stoppage, computer failure or 130 | malfunction, or any and all other commercial damages or losses), even if such Contributor has 131 | been advised of the possibility of such damages. 132 | 133 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works 134 | thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, 135 | indemnity, or other liability obligations and/or rights consistent with this License. However, in 136 | accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, 137 | not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each 138 | Contributor harmless for any liability incurred by, or claims asserted against, such Contributor 139 | by reason of your accepting any such warranty or additional liability. 140 | 141 | END OF TERMS AND CONDITIONS 142 | 143 | Copyright 2021 Rahul Behera 144 | 145 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in 146 | compliance with the License. You may obtain a copy of the License at 147 | 148 | http://www.apache.org/licenses/LICENSE-2.0 149 | 150 | Unless required by applicable law or agreed to in writing, software distributed under the License is 151 | distributed on an " 152 | AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 153 | License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![Contributors][contributors-shield]][contributors-url] 2 | [![Forks][forks-shield]][forks-url] 3 | [![Stargazers][stars-shield]][stars-url] 4 | [![Issues][issues-shield]][issues-url] 5 | [![MIT License][license-shield]][license-url] 6 | [![LinkedIn][linkedin-shield]][linkedin-url] 7 | 8 | 9 | 10 | 11 | 12 |
13 |

14 |

ByteBuffer

15 | 16 |

17 | A kotlin multiplatform library that allows you to allocate and modify byte[] natively using an API similar to Java's ByteBuffer API. 18 |
19 | 20 |
21 |
22 | 24 | Report Bug 25 | · 26 | Request Feature 27 |

28 | 29 | 30 |
31 | Table of Contents 32 |
    33 |
  1. 34 | About The Project 35 | 38 | 41 |
  2. 42 |
  3. Installation
  4. 43 |
  5. 44 | Usage 45 | 54 |
  6. 55 |
  7. 56 | Building Locally 57 |
  8. 58 |
  9. Getting Started
  10. 59 |
  11. Roadmap
  12. 60 |
  13. Contributing
  14. 61 |
  15. License
  16. 62 |
63 |
64 | 65 | ## About The Project 66 | 67 | Allocating and managing a chunk of memory can be slightly different based on each platform. This 68 | project aims to make it **easier to manage buffers in a cross platform way using kotlin 69 | multiplatform**. This was originally created as a side project for a kotlin multiplatform mqtt data 70 | sync solution. 71 | 72 | Implementation notes: 73 | 74 | * `JVM` + `Android` delegate to direct [ByteBuffers][byte-buffer-api] to avoid memory copies when 75 | possible. 76 | * Apple targets use NSData or NSMutableData 77 | * `JS` targets use Uint8Array. 78 | * `Native` platforms use standard byte arrays to manage memory. 79 | 80 | ### Runtime Dependencies 81 | 82 | * None 83 | 84 | ### [Supported Platforms](https://kotlinlang.org/docs/reference/mpp-supported-platforms.html) 85 | 86 | * All Kotlin Multiplatform supported OS's. 87 | 88 | | Platform | Wrapped Type | 89 | |:------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------:| 90 | | `JVM` 1.8 | [ByteBuffer](https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/nio/ByteBuffer.html) | 91 | | `Node.js` | [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) including SharedArrayBuffer | 92 | | `Browser` (Chrome) | [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) including SharedArrayBuffer | 93 | | `Android` | [ByteBuffer](https://developer.android.com/reference/java/nio/ByteBuffer) including [SharedMemory](https://developer.android.com/reference/android/os/SharedMemory) | 94 | | `iOS` | [NSData](https://developer.apple.com/documentation/foundation/nsdata?language=objc) | 95 | | `WatchOS` | [NSData](https://developer.apple.com/documentation/foundation/nsdata?language=objc) | 96 | | `TvOS` | [NSData](https://developer.apple.com/documentation/foundation/nsdata?language=objc) | 97 | | `MacOS` | [NSData](https://developer.apple.com/documentation/foundation/nsdata?language=objc) | 98 | | `Linux X64` | kotlin ByteArray | 99 | | `Windows X64` | TODO | 100 | 101 | ## Installation 102 | 103 | ### Gradle 104 | 105 | - [Add `implementation("com.ditchoom:buffer:$version")` to your `build.gradle` dependencies](https://search.maven.org/artifact/com.ditchoom/buffer) 106 | 107 | ## Usage 108 | 109 | ### Allocate a new platform agnostic buffer 110 | 111 | ```kotlin 112 | val buffer = PlatformBuffer.allocate( 113 | byteSize, 114 | zone = AllocationZone.Direct, 115 | byteOrder = ByteOrder.BIG_ENDIAN 116 | ) 117 | ``` 118 | 119 | ### Wrap an existing byte array into a platform agnostic buffer 120 | 121 | ```kotlin 122 | val byteArray = byteArrayOf(1, 2, 3, 4, 5) 123 | val buffer = PlatformBuffer.wrap(byteArray, byteOrder = ByteOrder.BIG_ENDIAN) 124 | ``` 125 | 126 | ### Allocation Zones 127 | 128 | Allocation zones allow you to change where the buffer is allocated. 129 | 130 | - `AllocationZone.Custom` -> Allows you to override the underlying buffer. This can be helpful for 131 | memory mapped structures. 132 | - `AllocationZone.Heap` -> On JVM platforms, allocates a HeapByteBuffer, otherwise a native byte 133 | array 134 | - `AllocationZone.Direct` -> On JVM platforms, allocates a DirectByteBuffer, otherwise a native byte 135 | array 136 | - `AllocationZone.SharedMemory` -> On JS Platforms this will populate the `sharedArrayBuffer` parameter in `JsBuffer`. 137 | On API 27+ it allocates 138 | a [Shared Memory](https://developer.android.com/reference/android/os/SharedMemory) instance, 139 | otherwise will pipe the data during parcel using ParcelFileDescriptor and java.nio.Channel api. For `JS` platforms it 140 | will allocate 141 | a [`SharedArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer). 142 | If the 143 | proper [security requirements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements) 144 | are not set, it will fallback to a standard `ArrayBuffer`. 145 | 146 | > **Android**: All `JvmBuffer`s are `Parcelable`. To avoid extra memory copies when using IPC, always choose `AllocationZone.SharedMemory`. 147 | 148 | > **Browser JS**: To enable SharedArrayBuffer, add the appropriate headers for the JS server in a gradle project by adding any file to a directory `webpack.config.d` next to the `src` directory containing: 149 | >``` 150 | >if (config.devServer != null) { 151 | > config.devServer.headers = { 152 | > "Cross-Origin-Opener-Policy": "same-origin", 153 | > "Cross-Origin-Embedder-Policy": "require-corp" 154 | > } 155 | >} 156 | >``` 157 | 158 | ### Byte order 159 | 160 | Byte order defaults to big endian but can be specified when creating the buffer 161 | with `ByteOrder.BIG_ENDIAN` 162 | or `ByteOrder.LITTLE_ENDIAN` 163 | 164 | The byte order of a buffer can be checked with `buffer.byteOrder` 165 | 166 | ### Relative write data into platform agnostic buffer 167 | 168 | ```kotlin 169 | val buffer: WriteBuffer 170 | // write signed byte 171 | buffer.writeByte(5.toByte()) 172 | // write unsigned byte 173 | buffer.writeUByte(5.toUByte()) 174 | // write short 175 | buffer.writeShort(5.toShort()) 176 | // write unsigned short 177 | buffer.writeUShort(5.toUShort()) 178 | // write int 179 | buffer.writeInt(5) 180 | // write unsigned int 181 | buffer.writeUInt(5.toUInt()) 182 | // write long 183 | buffer.writeLong(5L) 184 | // write unsigned long 185 | buffer.writeULong(5uL) 186 | // write float 187 | buffer.writeFloat(123.456f) 188 | // write double 189 | buffer.writeDouble(123.456) 190 | // write text 191 | buffer.writeString("5", Charset.UTF8) 192 | // copy buffer into this one 193 | buffer.write(otherBuffer) 194 | // write byte array 195 | buffer.writeBytes(byteArrayOf(1, 2, 3, 4)) 196 | // write partial byte array 197 | buffer.writeBytes(byteArrayOf(1, 2, 3, 4, 5), offset, length) 198 | ``` 199 | 200 | ### Absolute write data into platform agnostic buffer 201 | 202 | ```kotlin 203 | val buffer: WriteBuffer 204 | // set signed byte 205 | buffer[index] = 5.toByte() 206 | // set unsigned byte 207 | buffer[index] = 5.toUByte() 208 | // set short 209 | buffer[index] = 5.toByte() 210 | // set unsigned short 211 | buffer[index] = 5.toUShort() 212 | // set int 213 | buffer[index] = 5 214 | // set unsigned int 215 | buffer[index] = 5.toUInt() 216 | // set long 217 | buffer[index] = 5L 218 | // set unsigned long 219 | buffer[index] = 5uL 220 | // set float 221 | buffer[index] = 123.456f 222 | // set double 223 | buffer[index] = 123.456 224 | ``` 225 | 226 | ### Relative read data into platform agnostic buffer 227 | 228 | ```kotlin 229 | val buffer: ReadBuffer 230 | // read signed byte 231 | val b = buffer.readByte() 232 | // read unsigned byte 233 | val uByte = buffer.readUnsignedByte() 234 | // read short 235 | val short = buffer.readShort() 236 | // read unsigned short 237 | val uShort = buffer.readUnsignedShort() 238 | // read int 239 | val intValue = buffer.readInt() 240 | // read unsigned int 241 | val uIntValue = buffer.readUnsignedInt() 242 | // read long 243 | val longValue = buffer.readLong() 244 | // read unsigned long 245 | val uLongValue = buffer.readUnsignedLong() 246 | // read float 247 | val float = buffer.readFloat() 248 | // read double 249 | val double = buffer.readDouble() 250 | // read text 251 | val string = buffer.readUtf8(numOfBytesToRead) 252 | // read byte array 253 | val byteArray = buffer.readByteArray(numOfBytesToRead) 254 | // read a shared subsequence read buffer (changes to the original reflect here) 255 | val readBuffer = buffer.readBytes(numOfBytesForBuffer) 256 | ``` 257 | 258 | ### Absolute read data into platform agnostic buffer 259 | 260 | ```kotlin 261 | val buffer: ReadBuffer 262 | // get signed byte 263 | val b = buffer.get(index) // or buffer[index] 264 | // get unsigned byte 265 | val uByte = buffer.getUnsignedByte(index) 266 | // get short 267 | val short = buffer.getShort(index) 268 | // get unsigned short 269 | val uShort = buffer.getUnsignedShort(index) 270 | // get int 271 | val intValue = buffer.getInt(index) 272 | // get unsigned int 273 | val uIntValue = buffer.getUnsignedInt(index) 274 | // get long 275 | val longValue = buffer.getLong(index) 276 | // get unsigned long 277 | val uLongValue = buffer.getUnsignedLong(index) 278 | // get float 279 | val float = buffer.getFloat(index) 280 | // get double 281 | val double = buffer.getDouble(index) 282 | // slice the buffer without adjusting the position or limit (changes to the original reflect here) 283 | val slicedBuffer = buffer.slice() 284 | ``` 285 | 286 | ## Building Locally 287 | 288 | - `git clone git@github.com:DitchOoM/buffer.git` 289 | - Open cloned directory with [Intellij IDEA](https://www.jetbrains.com/idea/download). 290 | - Be sure 291 | to [open with gradle](https://www.jetbrains.com/help/idea/gradle.html#gradle_import_project_start) 292 | 293 | ## Roadmap 294 | 295 | See the [open issues](https://github.com/DitchOoM/buffer/issues) for a list of proposed features ( 296 | and known issues). 297 | 298 | ## Contributing 299 | 300 | Contributions are what make the open source community such an amazing place to be learn, inspire, 301 | and create. Any contributions you make are **greatly appreciated**. 302 | 303 | 1. Fork the Project 304 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 305 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 306 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 307 | 5. Open a Pull Request 308 | 309 | ## License 310 | 311 | Distributed under the Apache 2.0 License. See `LICENSE` for more information. 312 | 313 | [contributors-shield]: https://img.shields.io/github/contributors/DitchOoM/buffer.svg?style=for-the-badge 314 | 315 | [contributors-url]: https://github.com/DitchOoM/buffer/graphs/contributors 316 | 317 | [forks-shield]: https://img.shields.io/github/forks/DitchOoM/buffer.svg?style=for-the-badge 318 | 319 | [forks-url]: https://github.com/DitchOoM/buffer/network/members 320 | 321 | [stars-shield]: https://img.shields.io/github/stars/DitchOoM/buffer.svg?style=for-the-badge 322 | 323 | [stars-url]: https://github.com/DitchOoM/buffer/stargazers 324 | 325 | [issues-shield]: https://img.shields.io/github/issues/DitchOoM/buffer.svg?style=for-the-badge 326 | 327 | [issues-url]: https://github.com/DitchOoM/buffer/issues 328 | 329 | [license-shield]: https://img.shields.io/github/license/DitchOoM/buffer.svg?style=for-the-badge 330 | 331 | [license-url]: https://github.com/DitchOoM/buffer/blob/master/LICENSE.md 332 | 333 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 334 | 335 | [linkedin-url]: https://www.linkedin.com/in/thebehera 336 | 337 | [byte-buffer-api]: https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html 338 | 339 | [maven-central]: https://search.maven.org/search?q=com.ditchoom 340 | 341 | [npm]: https://www.npmjs.com/search?q=ditchoom-buffer 342 | 343 | [cocoapods]: https://cocoapods.org/pods/DitchOoM-buffer 344 | 345 | [apt]: https://packages.ubuntu.com/search?keywords=ditchoom&searchon=names&suite=groovy§ion=all 346 | 347 | [yum]: https://pkgs.org/search/?q=DitchOoM-buffer 348 | 349 | [chocolately]: https://chocolatey.org/packages?q=DitchOoM-buffer 350 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import groovy.util.Node 2 | import groovy.xml.XmlParser 3 | import org.apache.tools.ant.taskdefs.condition.Os 4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 5 | import java.net.URL 6 | 7 | plugins { 8 | kotlin("multiplatform") version "2.0.0" 9 | id("com.android.library") version "8.4.0" 10 | id("io.codearte.nexus-staging") version "0.30.0" 11 | `maven-publish` 12 | signing 13 | id("org.jlleitschuh.gradle.ktlint") version "12.1.1" 14 | } 15 | val isRunningOnGithub = System.getenv("GITHUB_REPOSITORY")?.isNotBlank() == true 16 | val isMainBranchGithub = System.getenv("GITHUB_REF") == "refs/heads/main" 17 | val isMacOS = Os.isFamily(Os.FAMILY_MAC) 18 | val loadAllPlatforms = !isRunningOnGithub || (isMacOS && isMainBranchGithub) || !isMacOS 19 | val libraryVersionPrefix: String by project 20 | group = "com.ditchoom" 21 | val libraryVersion = getNextVersion().toString() 22 | println( 23 | "Version: ${libraryVersion}\nisRunningOnGithub: $isRunningOnGithub\nisMainBranchGithub: $isMainBranchGithub\n" + 24 | "OS:$isMacOS\nLoad All Platforms: $loadAllPlatforms", 25 | ) 26 | 27 | repositories { 28 | google() 29 | mavenCentral() 30 | maven { setUrl("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers/") } 31 | } 32 | 33 | kotlin { 34 | androidTarget { 35 | publishLibraryVariants("release") 36 | compilerOptions.jvmTarget.set(JvmTarget.JVM_1_8) 37 | } 38 | jvm { 39 | compilerOptions.jvmTarget.set(JvmTarget.JVM_1_8) 40 | } 41 | js { 42 | moduleName = "buffer-kt" 43 | browser() 44 | nodejs() 45 | } 46 | macosX64() 47 | macosArm64() 48 | linuxX64() 49 | linuxArm64() 50 | iosArm64() 51 | iosSimulatorArm64() 52 | iosX64() 53 | watchosArm64() 54 | watchosSimulatorArm64() 55 | watchosX64() 56 | tvosArm64() 57 | tvosSimulatorArm64() 58 | tvosX64() 59 | applyDefaultHierarchyTemplate() 60 | sourceSets { 61 | commonTest.dependencies { 62 | implementation(kotlin("test")) 63 | } 64 | androidMain.dependencies { 65 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") 66 | } 67 | 68 | val androidInstrumentedTest by getting { 69 | dependencies { 70 | implementation(kotlin("test")) 71 | implementation("androidx.test:runner:1.5.2") 72 | implementation("androidx.test:rules:1.5.0") 73 | implementation("androidx.test:core-ktx:1.5.0") 74 | implementation("androidx.test.ext:junit:1.1.5") 75 | } 76 | } 77 | 78 | jsMain.dependencies { 79 | implementation("org.jetbrains.kotlin-wrappers:kotlin-web:1.0.0-pre.746") 80 | implementation("org.jetbrains.kotlin-wrappers:kotlin-js:1.0.0-pre.746") 81 | } 82 | } 83 | } 84 | 85 | android { 86 | buildFeatures { 87 | aidl = true 88 | } 89 | compileSdk = 34 90 | defaultConfig { 91 | minSdk = 16 92 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 93 | } 94 | namespace = "$group.${rootProject.name}" 95 | publishing { 96 | singleVariant("release") { 97 | withSourcesJar() 98 | withJavadocJar() 99 | } 100 | } 101 | } 102 | 103 | val javadocJar: TaskProvider by tasks.registering(Jar::class) { 104 | archiveClassifier.set("javadoc") 105 | } 106 | 107 | if (isRunningOnGithub) { 108 | if (isMainBranchGithub) { 109 | signing { 110 | useInMemoryPgpKeys( 111 | "56F1A973", 112 | System.getenv("GPG_SECRET"), 113 | System.getenv("GPG_SIGNING_PASSWORD"), 114 | ) 115 | sign(publishing.publications) 116 | } 117 | } 118 | 119 | val ossUser = System.getenv("SONATYPE_NEXUS_USERNAME") 120 | val ossPassword = System.getenv("SONATYPE_NEXUS_PASSWORD") 121 | 122 | val publishedGroupId: String by project 123 | val libraryName: String by project 124 | val libraryDescription: String by project 125 | val siteUrl: String by project 126 | val gitUrl: String by project 127 | val licenseName: String by project 128 | val licenseUrl: String by project 129 | val developerOrg: String by project 130 | val developerName: String by project 131 | val developerEmail: String by project 132 | val developerId: String by project 133 | 134 | project.group = publishedGroupId 135 | project.version = libraryVersion 136 | 137 | publishing { 138 | publications.withType(MavenPublication::class) { 139 | groupId = publishedGroupId 140 | version = libraryVersion 141 | 142 | artifact(tasks["javadocJar"]) 143 | 144 | pom { 145 | name.set(libraryName) 146 | description.set(libraryDescription) 147 | url.set(siteUrl) 148 | 149 | licenses { 150 | license { 151 | name.set(licenseName) 152 | url.set(licenseUrl) 153 | } 154 | } 155 | developers { 156 | developer { 157 | id.set(developerId) 158 | name.set(developerName) 159 | email.set(developerEmail) 160 | } 161 | } 162 | organization { 163 | name.set(developerOrg) 164 | } 165 | scm { 166 | connection.set(gitUrl) 167 | developerConnection.set(gitUrl) 168 | url.set(siteUrl) 169 | } 170 | } 171 | } 172 | 173 | repositories { 174 | val repositoryId = System.getenv("SONATYPE_REPOSITORY_ID") 175 | maven("https://oss.sonatype.org/service/local/staging/deployByRepositoryId/$repositoryId/") { 176 | name = "sonatype" 177 | credentials { 178 | username = ossUser 179 | password = ossPassword 180 | } 181 | } 182 | } 183 | } 184 | 185 | nexusStaging { 186 | username = ossUser 187 | password = ossPassword 188 | packageGroup = publishedGroupId 189 | } 190 | } 191 | 192 | ktlint { 193 | verbose.set(true) 194 | outputToConsole.set(true) 195 | } 196 | 197 | class Version(val major: UInt, val minor: UInt, val patch: UInt, val snapshot: Boolean) { 198 | constructor(string: String, snapshot: Boolean) : 199 | this( 200 | string.split('.')[0].toUInt(), 201 | string.split('.')[1].toUInt(), 202 | string.split('.')[2].toUInt(), 203 | snapshot, 204 | ) 205 | 206 | fun incrementMajor() = Version(major + 1u, 0u, 0u, snapshot) 207 | 208 | fun incrementMinor() = Version(major, minor + 1u, 0u, snapshot) 209 | 210 | fun incrementPatch() = Version(major, minor, patch + 1u, snapshot) 211 | 212 | fun snapshot() = Version(major, minor, patch, true) 213 | 214 | fun isVersionZero() = major == 0u && minor == 0u && patch == 0u 215 | 216 | override fun toString(): String = 217 | if (snapshot) { 218 | "$major.$minor.$patch-SNAPSHOT" 219 | } else { 220 | "$major.$minor.$patch" 221 | } 222 | } 223 | private var latestVersion: Version? = Version(0u, 0u, 0u, true) 224 | 225 | @Suppress("UNCHECKED_CAST") 226 | fun getLatestVersion(): Version { 227 | val latestVersion = latestVersion 228 | if (latestVersion != null && !latestVersion.isVersionZero()) { 229 | return latestVersion 230 | } 231 | val xml = URL("https://repo1.maven.org/maven2/com/ditchoom/${rootProject.name}/maven-metadata.xml").readText() 232 | val versioning = XmlParser().parseText(xml)["versioning"] as List 233 | val latestStringList = versioning.first()["latest"] as List 234 | val result = Version((latestStringList.first().value() as List<*>).first().toString(), false) 235 | this.latestVersion = result 236 | return result 237 | } 238 | 239 | fun getNextVersion(snapshot: Boolean = !isRunningOnGithub): Version { 240 | var v = getLatestVersion() 241 | if (snapshot) { 242 | v = v.snapshot() 243 | } 244 | if (project.hasProperty("incrementMajor") && project.property("incrementMajor") == "true") { 245 | return v.incrementMajor() 246 | } else if (project.hasProperty("incrementMinor") && project.property("incrementMinor") == "true") { 247 | return v.incrementMinor() 248 | } 249 | return v.incrementPatch() 250 | } 251 | 252 | tasks.create("nextVersion") { 253 | println(getNextVersion()) 254 | } 255 | 256 | val signingTasks = tasks.withType() 257 | tasks.withType().configureEach { 258 | dependsOn(signingTasks) 259 | } 260 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-XX:MaxMetaspaceSize=4g 4 | kotlin.mpp.stability.nowarn=true 5 | libraryVersionPrefix=1.3. 6 | libraryName=Buffer 7 | libraryDescription=Multiplatform bytebuffer that delegates to native byte[] or ByteBuffer 8 | publishedGroupId=com.ditchoom 9 | artifactName=buffer 10 | siteUrl=https://github.com/DitchOoM/buffer 11 | gitUrl=git://github.com/DitchOOM/buffer.git 12 | developerId=thebehera 13 | developerOrg=Ditchoom 14 | developerName=Rahul Behera 15 | developerEmail=rbehera@gmail.com 16 | licenseName=The Apache Software License, Version 2.0 17 | licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.txt 18 | allLicenses=["Apache-2.0"] 19 | 20 | kotlin.mpp.androidGradlePluginCompatibility.nowarn=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DitchOoM/buffer/56bae8919a09ab89ba831bc8862200735c0f4f61/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /karma.config.d/karma.conf.js: -------------------------------------------------------------------------------- 1 | config.client = config.client || {} 2 | config.client.mocha = config.client.mocha || {} 3 | config.client.mocha.timeout = 10000 4 | config.browserNoActivityTimeout = 10000 5 | config.browserDisconnectTimeout = 10000 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | } 7 | } 8 | rootProject.name = "buffer" 9 | plugins { 10 | id("com.gradle.develocity") version ("3.17.3") 11 | } 12 | develocity { 13 | buildScan { 14 | uploadInBackground.set(System.getenv("CI") != null) 15 | termsOfUseUrl.set("https://gradle.com/help/legal-terms-of-use") 16 | termsOfUseAgree.set("yes") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/androidInstrumentedTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /src/androidInstrumentedTest/aidl/com/ditchoom/buffer/BufferIpcTestAidl.aidl: -------------------------------------------------------------------------------- 1 | // BufferIpcTestAidl.aidl 2 | package com.ditchoom.buffer; 3 | 4 | import com.ditchoom.buffer.JvmBuffer; 5 | 6 | interface BufferIpcTestAidl { 7 | JvmBuffer aidl(int num, int type); 8 | } -------------------------------------------------------------------------------- /src/androidInstrumentedTest/kotlin/com/ditchoom/buffer/IPCSimpleService.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.os.IBinder 6 | 7 | class IPCSimpleService : Service() { 8 | override fun onBind(intent: Intent?): IBinder = 9 | object : BufferIpcTestAidl.Stub() { 10 | private val byteSize = 4 * 1024 * 1024 11 | 12 | override fun aidl( 13 | num: Int, 14 | type: Int, 15 | ): JvmBuffer { 16 | val zone = 17 | when (type) { 18 | 0 -> AllocationZone.Heap 19 | 1 -> AllocationZone.Direct 20 | else -> AllocationZone.SharedMemory 21 | } 22 | val buffer = PlatformBuffer.allocate(byteSize, zone) 23 | buffer.writeInt(num) 24 | return buffer as JvmBuffer 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/androidInstrumentedTest/kotlin/com/ditchoom/buffer/IPCTest.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.ServiceConnection 7 | import android.os.IBinder 8 | import androidx.test.ext.junit.runners.AndroidJUnit4 9 | import androidx.test.filters.MediumTest 10 | import androidx.test.platform.app.InstrumentationRegistry 11 | import androidx.test.rule.ServiceTestRule 12 | import kotlinx.coroutines.runBlocking 13 | import kotlinx.coroutines.sync.Mutex 14 | import org.junit.Rule 15 | import org.junit.Test 16 | import org.junit.runner.RunWith 17 | import java.util.concurrent.TimeUnit 18 | import kotlin.random.Random 19 | import kotlin.test.assertEquals 20 | import kotlin.test.assertFalse 21 | import kotlin.test.assertTrue 22 | 23 | @RunWith(AndroidJUnit4::class) 24 | @MediumTest 25 | class IPCTest { 26 | @get:Rule 27 | val serviceRule: ServiceTestRule = ServiceTestRule.withTimeout(3, TimeUnit.SECONDS) 28 | 29 | @Test 30 | fun sharedMemoryBuffer() = 31 | runBlocking { 32 | val mutex = Mutex(true) 33 | lateinit var stub: BufferIpcTestAidl 34 | serviceRule.bindService( 35 | Intent(InstrumentationRegistry.getInstrumentation().context, IPCSimpleService::class.java), 36 | object : ServiceConnection { 37 | override fun onServiceConnected( 38 | name: ComponentName, 39 | service: IBinder, 40 | ) { 41 | stub = BufferIpcTestAidl.Stub.asInterface(service) 42 | mutex.unlock() 43 | } 44 | 45 | override fun onServiceDisconnected(name: ComponentName) { 46 | throw IllegalStateException("Service Disconnected") 47 | } 48 | }, 49 | Context.BIND_AUTO_CREATE, 50 | ) 51 | mutex.lock() 52 | val num = Random.nextInt() 53 | val buffer2 = stub.aidl(num, 2) 54 | assertTrue { buffer2.byteBuffer.isDirect } 55 | buffer2.resetForRead() 56 | assertEquals(num, buffer2.readInt()) 57 | } 58 | 59 | @Test 60 | fun copyJvmDirect() = 61 | runBlocking { 62 | val mutex = Mutex(true) 63 | lateinit var stub: BufferIpcTestAidl 64 | serviceRule.bindService( 65 | Intent(InstrumentationRegistry.getInstrumentation().context, IPCSimpleService::class.java), 66 | object : ServiceConnection { 67 | override fun onServiceConnected( 68 | name: ComponentName, 69 | service: IBinder, 70 | ) { 71 | stub = BufferIpcTestAidl.Stub.asInterface(service) 72 | mutex.unlock() 73 | } 74 | 75 | override fun onServiceDisconnected(name: ComponentName) { 76 | throw IllegalStateException("Service Disconnected") 77 | } 78 | }, 79 | Context.BIND_AUTO_CREATE, 80 | ) 81 | mutex.lock() 82 | val num = Random.nextInt() 83 | val buffer1 = stub.aidl(num, 1) 84 | assertTrue { buffer1.byteBuffer.isDirect } 85 | buffer1.resetForRead() 86 | assertEquals(num, buffer1.readInt()) 87 | } 88 | 89 | @Test 90 | fun copyJvmHeap() = 91 | runBlocking { 92 | val mutex = Mutex(true) 93 | lateinit var stub: BufferIpcTestAidl 94 | serviceRule.bindService( 95 | Intent(InstrumentationRegistry.getInstrumentation().context, IPCSimpleService::class.java), 96 | object : ServiceConnection { 97 | override fun onServiceConnected( 98 | name: ComponentName, 99 | service: IBinder, 100 | ) { 101 | stub = BufferIpcTestAidl.Stub.asInterface(service) 102 | mutex.unlock() 103 | } 104 | 105 | override fun onServiceDisconnected(name: ComponentName) { 106 | throw IllegalStateException("Service Disconnected") 107 | } 108 | }, 109 | Context.BIND_AUTO_CREATE, 110 | ) 111 | mutex.lock() 112 | val num = Random.nextInt() 113 | val buffer0 = stub.aidl(num, 0) 114 | assertFalse { buffer0.byteBuffer.isDirect } 115 | buffer0.resetForRead() 116 | assertEquals(num, buffer0.readInt()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/androidInstrumentedTest/kotlin/com/ditchoom/buffer/ParcelableTests.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import android.os.Build 4 | import android.os.Parcel 5 | import android.os.SharedMemory 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import androidx.test.filters.SmallTest 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import java.nio.ByteBuffer 11 | import kotlin.random.Random 12 | import kotlin.test.assertEquals 13 | 14 | @RunWith(AndroidJUnit4::class) 15 | @SmallTest 16 | class ParcelableTests { 17 | @Test 18 | fun parcelSharedMemory() { 19 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return 20 | val sharedMemoryBuffer = SharedMemory.create("test", 2_000_000) 21 | val s = ParcelableSharedMemoryBuffer(sharedMemoryBuffer.mapReadWrite(), sharedMemoryBuffer) 22 | val i = Random.nextInt() 23 | s.writeInt(i) 24 | 25 | val parcel = Parcel.obtain() 26 | s.writeToParcel(parcel, Parcelable.PARCELABLE_WRITE_RETURN_VALUE) 27 | parcel.setDataPosition(0) 28 | 29 | val createdFromParcel = ParcelableSharedMemoryBuffer.CREATOR.createFromParcel(parcel) 30 | createdFromParcel.resetForRead() 31 | assertEquals(i, createdFromParcel.readInt()) 32 | } 33 | 34 | @Test 35 | fun parcelFileDescriptor() { 36 | val buffer = ByteBuffer.allocateDirect(2_000_000) 37 | val s = JvmBuffer(buffer) 38 | val i = Random.nextInt() 39 | s.writeInt(i) 40 | 41 | val parcel = Parcel.obtain() 42 | s.writeToParcel(parcel, Parcelable.PARCELABLE_WRITE_RETURN_VALUE) 43 | parcel.setDataPosition(0) 44 | 45 | val createdFromParcel = JvmBuffer.CREATOR.createFromParcel(parcel) 46 | createdFromParcel.resetForRead() 47 | assertEquals(i, createdFromParcel.readInt()) 48 | } 49 | 50 | @Test 51 | fun copyByteArray() { 52 | val s = JvmBuffer(ByteBuffer.allocate(4_000)) 53 | val i = Random.nextInt() 54 | s.writeInt(i) 55 | 56 | val parcel = Parcel.obtain() 57 | s.writeToParcel(parcel, 0) 58 | parcel.setDataPosition(0) 59 | 60 | val createdFromParcel = JvmBuffer.CREATOR.createFromParcel(parcel) 61 | createdFromParcel.resetForRead() 62 | assertEquals(i, createdFromParcel.readInt()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/androidMain/aidl/com/ditchoom/buffer/JvmBuffer.aidl: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer; 2 | 3 | parcelable JvmBuffer; -------------------------------------------------------------------------------- /src/androidMain/kotlin/com/ditchoom/buffer/BaseJvmBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import java.io.RandomAccessFile 4 | import java.nio.Buffer 5 | import java.nio.ByteBuffer 6 | import java.nio.CharBuffer 7 | import java.nio.charset.CodingErrorAction 8 | import kotlin.coroutines.resume 9 | import kotlin.coroutines.resumeWithException 10 | import kotlin.coroutines.suspendCoroutine 11 | 12 | abstract class BaseJvmBuffer(open val byteBuffer: ByteBuffer, val fileRef: RandomAccessFile? = null) : 13 | PlatformBuffer { 14 | override val byteOrder = 15 | when (byteBuffer.order()) { 16 | java.nio.ByteOrder.BIG_ENDIAN -> ByteOrder.BIG_ENDIAN 17 | java.nio.ByteOrder.LITTLE_ENDIAN -> ByteOrder.LITTLE_ENDIAN 18 | else -> ByteOrder.BIG_ENDIAN 19 | } 20 | 21 | // Use Buffer reference to avoid NoSuchMethodException between JVM. see https://stackoverflow.com/q/61267495 22 | private val buffer = byteBuffer as Buffer 23 | 24 | override fun resetForRead() { 25 | buffer.flip() 26 | } 27 | 28 | override fun resetForWrite() { 29 | buffer.clear() 30 | } 31 | 32 | override fun setLimit(limit: Int) { 33 | buffer.limit(limit) 34 | } 35 | 36 | override val capacity = buffer.capacity() 37 | 38 | override fun readByte() = byteBuffer.get() 39 | 40 | override fun get(index: Int): Byte = byteBuffer.get(index) 41 | 42 | override fun readByteArray(size: Int) = byteBuffer.toArray(size) 43 | 44 | override fun slice() = JvmBuffer(byteBuffer.slice()) 45 | 46 | override fun readShort(): Short = byteBuffer.short 47 | 48 | override fun getShort(index: Int): Short = byteBuffer.getShort(index) 49 | 50 | override fun readInt() = byteBuffer.int 51 | 52 | override fun getInt(index: Int): Int = byteBuffer.getInt(index) 53 | 54 | override fun readLong() = byteBuffer.long 55 | 56 | override fun getLong(index: Int): Long = byteBuffer.getLong(index) 57 | 58 | override fun readString( 59 | length: Int, 60 | charset: Charset, 61 | ): String { 62 | val finalPosition = buffer.position() + length 63 | val readBuffer = byteBuffer.asReadOnlyBuffer() 64 | (readBuffer as Buffer).limit(finalPosition) 65 | val charsetConverted = 66 | when (charset) { 67 | Charset.UTF8 -> Charsets.UTF_8 68 | Charset.UTF16 -> Charsets.UTF_16 69 | Charset.UTF16BigEndian -> Charsets.UTF_16BE 70 | Charset.UTF16LittleEndian -> Charsets.UTF_16LE 71 | Charset.ASCII -> Charsets.US_ASCII 72 | Charset.ISOLatin1 -> Charsets.ISO_8859_1 73 | Charset.UTF32 -> Charsets.UTF_32 74 | Charset.UTF32LittleEndian -> Charsets.UTF_32LE 75 | Charset.UTF32BigEndian -> Charsets.UTF_32BE 76 | } 77 | val decoded = 78 | charsetConverted.newDecoder() 79 | .onMalformedInput(CodingErrorAction.REPORT) 80 | .onUnmappableCharacter(CodingErrorAction.REPORT) 81 | .decode(readBuffer) 82 | buffer.position(finalPosition) 83 | return decoded.toString() 84 | } 85 | 86 | override fun writeByte(byte: Byte): WriteBuffer { 87 | byteBuffer.put(byte) 88 | return this 89 | } 90 | 91 | override fun set( 92 | index: Int, 93 | byte: Byte, 94 | ): WriteBuffer { 95 | byteBuffer.put(index, byte) 96 | return this 97 | } 98 | 99 | override fun writeBytes( 100 | bytes: ByteArray, 101 | offset: Int, 102 | length: Int, 103 | ): WriteBuffer { 104 | byteBuffer.put(bytes, offset, length) 105 | return this 106 | } 107 | 108 | override fun writeShort(short: Short): WriteBuffer { 109 | byteBuffer.putShort(short) 110 | return this 111 | } 112 | 113 | override fun set( 114 | index: Int, 115 | short: Short, 116 | ): WriteBuffer { 117 | byteBuffer.putShort(index, short) 118 | return this 119 | } 120 | 121 | override fun writeInt(int: Int): WriteBuffer { 122 | byteBuffer.putInt(int) 123 | return this 124 | } 125 | 126 | override fun set( 127 | index: Int, 128 | int: Int, 129 | ): WriteBuffer { 130 | byteBuffer.putInt(index, int) 131 | return this 132 | } 133 | 134 | override fun writeLong(long: Long): WriteBuffer { 135 | byteBuffer.putLong(long) 136 | return this 137 | } 138 | 139 | override fun set( 140 | index: Int, 141 | long: Long, 142 | ): WriteBuffer { 143 | byteBuffer.putLong(index, long) 144 | return this 145 | } 146 | 147 | override fun writeString( 148 | text: CharSequence, 149 | charset: Charset, 150 | ): WriteBuffer { 151 | val encoder = charset.toEncoder() 152 | encoder.reset() 153 | encoder.encode(CharBuffer.wrap(text), byteBuffer, true) 154 | return this 155 | } 156 | 157 | override fun write(buffer: ReadBuffer) { 158 | if (buffer is JvmBuffer) { 159 | byteBuffer.put(buffer.byteBuffer) 160 | } else { 161 | byteBuffer.put(buffer.readByteArray(buffer.remaining())) 162 | } 163 | } 164 | 165 | override fun position(newPosition: Int) { 166 | buffer.position(newPosition) 167 | } 168 | 169 | override fun equals(other: Any?): Boolean { 170 | if (other !is PlatformBuffer) return false 171 | if (position() != other.position()) return false 172 | if (limit() != other.limit()) return false 173 | if (capacity != other.capacity) return false 174 | return true 175 | } 176 | 177 | override fun toString() = "Buffer[pos=${position()} lim=${limit()} cap=$capacity]" 178 | 179 | override suspend fun close() { 180 | fileRef?.aClose() 181 | } 182 | 183 | override fun limit() = buffer.limit() 184 | 185 | override fun position() = buffer.position() 186 | } 187 | 188 | suspend fun RandomAccessFile.aClose() = 189 | suspendCoroutine { 190 | try { 191 | // TODO: fix the blocking call 192 | @Suppress("BlockingMethodInNonBlockingContext") 193 | close() 194 | it.resume(Unit) 195 | } catch (e: Throwable) { 196 | it.resumeWithException(e) 197 | } 198 | } 199 | 200 | fun ByteBuffer.toArray(size: Int = remaining()): ByteArray { 201 | return if (hasArray()) { 202 | val result = ByteArray(size) 203 | val buffer = this as Buffer 204 | System.arraycopy(this.array(), buffer.arrayOffset() + buffer.position(), result, 0, size) 205 | buffer.position(buffer.position() + size) 206 | result 207 | } else { 208 | val byteArray = ByteArray(size) 209 | get(byteArray) 210 | byteArray 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/androidMain/kotlin/com/ditchoom/buffer/BufferFactory.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("BufferFactoryAndroid") 2 | 3 | package com.ditchoom.buffer 4 | 5 | import android.os.Build 6 | import android.os.SharedMemory 7 | import java.nio.ByteBuffer 8 | 9 | actual fun PlatformBuffer.Companion.allocate( 10 | size: Int, 11 | zone: AllocationZone, 12 | byteOrder: ByteOrder, 13 | ): PlatformBuffer { 14 | val byteOrderNative = 15 | when (byteOrder) { 16 | ByteOrder.BIG_ENDIAN -> java.nio.ByteOrder.BIG_ENDIAN 17 | ByteOrder.LITTLE_ENDIAN -> java.nio.ByteOrder.LITTLE_ENDIAN 18 | } 19 | return when (zone) { 20 | AllocationZone.Heap -> JvmBuffer(ByteBuffer.allocate(size).order(byteOrderNative)) 21 | AllocationZone.Direct -> JvmBuffer(ByteBuffer.allocateDirect(size).order(byteOrderNative)) 22 | AllocationZone.SharedMemory -> 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && size > 0) { 24 | val sharedMemory = SharedMemory.create(null, size) 25 | val buffer = sharedMemory.mapReadWrite().order(byteOrderNative) 26 | ParcelableSharedMemoryBuffer(buffer, sharedMemory) 27 | } else { 28 | JvmBuffer(ByteBuffer.allocateDirect(size).order(byteOrderNative)) 29 | } 30 | 31 | is AllocationZone.Custom -> zone.allocator(size) 32 | } 33 | } 34 | 35 | actual fun PlatformBuffer.Companion.wrap( 36 | array: ByteArray, 37 | byteOrder: ByteOrder, 38 | ): PlatformBuffer { 39 | val byteOrderNative = 40 | when (byteOrder) { 41 | ByteOrder.BIG_ENDIAN -> java.nio.ByteOrder.BIG_ENDIAN 42 | ByteOrder.LITTLE_ENDIAN -> java.nio.ByteOrder.LITTLE_ENDIAN 43 | } 44 | return JvmBuffer(ByteBuffer.wrap(array).order(byteOrderNative)) 45 | } 46 | -------------------------------------------------------------------------------- /src/androidMain/kotlin/com/ditchoom/buffer/CharsetEncoderHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import java.nio.charset.CharsetEncoder 4 | 5 | fun Charset.toEncoder(): CharsetEncoder = 6 | when (this) { 7 | Charset.UTF8 -> utf8Encoder 8 | Charset.UTF16 -> utf16Encoder 9 | Charset.UTF16BigEndian -> utf16BEEncoder 10 | Charset.UTF16LittleEndian -> utf16LEEncoder 11 | Charset.ASCII -> utfAsciiEncoder 12 | Charset.ISOLatin1 -> utfISOLatin1Encoder 13 | Charset.UTF32 -> utf32Encoder 14 | Charset.UTF32LittleEndian -> utf32LEEncoder 15 | Charset.UTF32BigEndian -> utf32BEEncoder 16 | }.get() 17 | 18 | internal class DefaultEncoder(private val charset: java.nio.charset.Charset) : 19 | ThreadLocal() { 20 | override fun initialValue(): CharsetEncoder? = charset.newEncoder() 21 | 22 | override fun get(): CharsetEncoder = super.get()!! 23 | } 24 | 25 | internal val utf8Encoder = DefaultEncoder(Charsets.UTF_8) 26 | internal val utf16Encoder = DefaultEncoder(Charsets.UTF_16) 27 | internal val utf16BEEncoder = DefaultEncoder(Charsets.UTF_16BE) 28 | internal val utf16LEEncoder = DefaultEncoder(Charsets.UTF_16LE) 29 | internal val utfAsciiEncoder = DefaultEncoder(Charsets.US_ASCII) 30 | internal val utfISOLatin1Encoder = DefaultEncoder(Charsets.ISO_8859_1) 31 | internal val utf32Encoder = DefaultEncoder(Charsets.UTF_32) 32 | internal val utf32LEEncoder = DefaultEncoder(Charsets.UTF_32LE) 33 | internal val utf32BEEncoder = DefaultEncoder(Charsets.UTF_32BE) 34 | -------------------------------------------------------------------------------- /src/androidMain/kotlin/com/ditchoom/buffer/JvmBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import android.os.Build 4 | import android.os.Parcel 5 | import android.os.ParcelFileDescriptor 6 | import android.os.Parcelable.Creator 7 | import kotlinx.coroutines.CoroutineName 8 | import kotlinx.coroutines.CoroutineScope 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.cancel 11 | import kotlinx.coroutines.launch 12 | import java.io.FileInputStream 13 | import java.io.FileOutputStream 14 | import java.nio.ByteBuffer 15 | 16 | open class JvmBuffer(val buffer: ByteBuffer) : BaseJvmBuffer(buffer) { 17 | override fun describeContents(): Int = 0 18 | 19 | override fun writeToParcel( 20 | dest: Parcel, 21 | flags: Int, 22 | ) { 23 | if (this is ParcelableSharedMemoryBuffer) { 24 | dest.writeByte(1) 25 | return 26 | } else { 27 | dest.writeByte(0) 28 | } 29 | dest.writeInt(byteBuffer.position()) 30 | dest.writeInt(byteBuffer.limit()) 31 | if (byteBuffer.isDirect) { 32 | dest.writeByte(1) 33 | } else { 34 | dest.writeByte(0) 35 | } 36 | byteBuffer.position(0) 37 | byteBuffer.limit(byteBuffer.capacity()) 38 | 39 | val (readFileDescriptor, writeFileDescriptor) = 40 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 41 | ParcelFileDescriptor.createReliablePipe() 42 | } else { 43 | ParcelFileDescriptor.createPipe() 44 | } 45 | readFileDescriptor.writeToParcel(dest, 0) 46 | val scope = CoroutineScope(Dispatchers.IO + CoroutineName("IPC Write Channel Jvm Buffer")) 47 | scope.launch { 48 | FileOutputStream(writeFileDescriptor.fileDescriptor).channel.use { writeChannel -> 49 | writeChannel.write(buffer) 50 | } 51 | writeFileDescriptor.close() 52 | readFileDescriptor.close() 53 | scope.cancel() 54 | } 55 | } 56 | 57 | companion object CREATOR : Creator { 58 | override fun createFromParcel(parcel: Parcel): JvmBuffer { 59 | val p = parcel.dataPosition() 60 | if (parcel.readByte().toInt() == 1) { 61 | parcel.setDataPosition(p) 62 | return ParcelableSharedMemoryBuffer.createFromParcel(parcel) 63 | } 64 | val position = parcel.readInt() 65 | val limit = parcel.readInt() 66 | val isDirect = parcel.readByte() == 1.toByte() 67 | val buffer = 68 | if (isDirect) { 69 | ByteBuffer.allocateDirect(limit) 70 | } else { 71 | ByteBuffer.allocate(limit) 72 | } 73 | ParcelFileDescriptor.CREATOR.createFromParcel(parcel).use { pfd -> 74 | FileInputStream(pfd.fileDescriptor).channel.use { readChannel -> readChannel.read(buffer) } 75 | } 76 | buffer.position(position) 77 | buffer.limit(limit) 78 | return JvmBuffer(buffer) 79 | } 80 | 81 | override fun newArray(size: Int): Array { 82 | return arrayOfNulls(size) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/androidMain/kotlin/com/ditchoom/buffer/Parcelable.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import android.os.Parcelable 4 | 5 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 6 | actual typealias Parcelable = Parcelable 7 | -------------------------------------------------------------------------------- /src/androidMain/kotlin/com/ditchoom/buffer/ParcelableSharedMemoryBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import android.annotation.TargetApi 4 | import android.os.Build 5 | import android.os.Parcel 6 | import android.os.Parcelable 7 | import android.os.SharedMemory 8 | import java.nio.ByteBuffer 9 | 10 | @TargetApi(Build.VERSION_CODES.O_MR1) 11 | class ParcelableSharedMemoryBuffer(buffer: ByteBuffer, private val sharedMemory: SharedMemory) : 12 | JvmBuffer(buffer), Parcelable { 13 | override fun describeContents(): Int = Parcelable.CONTENTS_FILE_DESCRIPTOR 14 | 15 | override fun writeToParcel( 16 | dest: Parcel, 17 | flags: Int, 18 | ) { 19 | super.writeToParcel(dest, flags) 20 | dest.writeParcelable(sharedMemory, flags) 21 | dest.writeInt(buffer.position()) 22 | dest.writeInt(buffer.limit()) 23 | } 24 | 25 | override suspend fun close() { 26 | super.close() 27 | sharedMemory.close() 28 | } 29 | 30 | companion object CREATOR : Parcelable.Creator { 31 | override fun createFromParcel(parcel: Parcel): ParcelableSharedMemoryBuffer { 32 | parcel.readByte() // ignore this first byte 33 | val sharedMemory = 34 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 35 | parcel.readParcelable(this::class.java.classLoader, SharedMemory::class.java) 36 | } else { 37 | @Suppress("DEPRECATION") 38 | parcel.readParcelable(this::class.java.classLoader) 39 | }!! 40 | val buffer = 41 | ParcelableSharedMemoryBuffer(sharedMemory.mapReadWrite(), sharedMemory) 42 | buffer.position(parcel.readInt()) 43 | buffer.setLimit(parcel.readInt()) 44 | return buffer 45 | } 46 | 47 | override fun newArray(size: Int): Array = arrayOfNulls(size) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/appleMain/kotlin/com/ditchoom/buffer/BufferFactory.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, kotlinx.cinterop.BetaInteropApi::class) 2 | 3 | package com.ditchoom.buffer 4 | 5 | import kotlinx.cinterop.UnsafeNumber 6 | import kotlinx.cinterop.convert 7 | import platform.Foundation.NSMutableData 8 | import platform.Foundation.create 9 | 10 | actual fun PlatformBuffer.Companion.allocate( 11 | size: Int, 12 | zone: AllocationZone, 13 | byteOrder: ByteOrder, 14 | ): PlatformBuffer { 15 | if (zone is AllocationZone.Custom) { 16 | return zone.allocator(size) 17 | } 18 | 19 | @OptIn(UnsafeNumber::class) 20 | return MutableDataBuffer(NSMutableData.create(length = size.convert())!!, byteOrder = byteOrder) 21 | } 22 | 23 | actual fun PlatformBuffer.Companion.wrap( 24 | array: ByteArray, 25 | byteOrder: ByteOrder, 26 | ): PlatformBuffer = MutableDataBuffer.wrap(array, byteOrder) 27 | -------------------------------------------------------------------------------- /src/appleMain/kotlin/com/ditchoom/buffer/CharsetEncoderHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import kotlinx.cinterop.UnsafeNumber 4 | import platform.Foundation.NSASCIIStringEncoding 5 | import platform.Foundation.NSISOLatin1StringEncoding 6 | import platform.Foundation.NSStringEncoding 7 | import platform.Foundation.NSUTF16BigEndianStringEncoding 8 | import platform.Foundation.NSUTF16LittleEndianStringEncoding 9 | import platform.Foundation.NSUTF16StringEncoding 10 | import platform.Foundation.NSUTF32BigEndianStringEncoding 11 | import platform.Foundation.NSUTF32LittleEndianStringEncoding 12 | import platform.Foundation.NSUTF32StringEncoding 13 | import platform.Foundation.NSUTF8StringEncoding 14 | 15 | @OptIn(UnsafeNumber::class) 16 | fun Charset.toEncoding(): NSStringEncoding = 17 | when (this) { 18 | Charset.UTF8 -> NSUTF8StringEncoding 19 | Charset.UTF16 -> NSUTF16StringEncoding 20 | Charset.UTF16BigEndian -> NSUTF16BigEndianStringEncoding 21 | Charset.UTF16LittleEndian -> NSUTF16LittleEndianStringEncoding 22 | Charset.ASCII -> NSASCIIStringEncoding 23 | Charset.ISOLatin1 -> NSISOLatin1StringEncoding 24 | Charset.UTF32 -> NSUTF32StringEncoding 25 | Charset.UTF32LittleEndian -> NSUTF32LittleEndianStringEncoding 26 | Charset.UTF32BigEndian -> NSUTF32BigEndianStringEncoding 27 | } 28 | -------------------------------------------------------------------------------- /src/appleMain/kotlin/com/ditchoom/buffer/DataBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import kotlinx.cinterop.ByteVar 4 | import kotlinx.cinterop.CPointer 5 | import kotlinx.cinterop.UnsafeNumber 6 | import kotlinx.cinterop.convert 7 | import kotlinx.cinterop.get 8 | import kotlinx.cinterop.readBytes 9 | import platform.Foundation.NSData 10 | import platform.Foundation.NSMakeRange 11 | import platform.Foundation.NSString 12 | import platform.Foundation.create 13 | import platform.Foundation.isEqualToData 14 | import platform.Foundation.subdataWithRange 15 | 16 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, UnsafeNumber::class) 17 | open class DataBuffer( 18 | val data: NSData, 19 | override val byteOrder: ByteOrder, 20 | ) : ReadBuffer, SuspendCloseable, Parcelable { 21 | protected var position: Int = 0 22 | protected var limit: Int = data.length.toInt() 23 | open val capacity: Int = data.length.toInt() 24 | 25 | @Suppress("UNCHECKED_CAST") 26 | private val bytePointer = this.data.bytes as CPointer 27 | 28 | override fun resetForRead() { 29 | limit = position 30 | position = 0 31 | } 32 | 33 | override fun setLimit(limit: Int) { 34 | this.limit = limit 35 | } 36 | 37 | override fun readByte() = bytePointer[position++] 38 | 39 | override fun get(index: Int): Byte = bytePointer[index] 40 | 41 | override fun slice(): ReadBuffer { 42 | val range = NSMakeRange(position.convert(), (limit - position).convert()) 43 | return DataBuffer(data.subdataWithRange(range), byteOrder = byteOrder) 44 | } 45 | 46 | override fun readByteArray(size: Int): ByteArray { 47 | if (size < 1) { 48 | return ByteArray(0) 49 | } 50 | val subDataRange = NSMakeRange(position.convert(), size.convert()) 51 | 52 | @Suppress("UNCHECKED_CAST") 53 | val subDataBytePointer = data.subdataWithRange(subDataRange).bytes as CPointer 54 | val result = subDataBytePointer.readBytes(size) 55 | position += size 56 | return result 57 | } 58 | 59 | override fun readString( 60 | length: Int, 61 | charset: Charset, 62 | ): String { 63 | if (length == 0) return "" 64 | val subdata = data.subdataWithRange(NSMakeRange(position.convert(), length.convert())) 65 | val stringEncoding = charset.toEncoding() 66 | 67 | @Suppress("CAST_NEVER_SUCCEEDS") 68 | @OptIn(kotlinx.cinterop.BetaInteropApi::class) 69 | val string = NSString.create(subdata, stringEncoding) as String 70 | position += length 71 | return string 72 | } 73 | 74 | override fun limit() = limit 75 | 76 | override fun position() = position 77 | 78 | override fun position(newPosition: Int) { 79 | position = newPosition 80 | } 81 | 82 | override suspend fun close() = Unit 83 | 84 | override fun equals(other: Any?): Boolean { 85 | if (this === other) return true 86 | if (other !is DataBuffer) return false 87 | if (position != other.position) return false 88 | if (limit != other.limit) return false 89 | if (capacity != other.capacity) return false 90 | if (!data.isEqualToData(other.data)) return false 91 | return true 92 | } 93 | 94 | override fun hashCode(): Int { 95 | var result = data.hashCode() 96 | result = 31 * result + position.hashCode() 97 | result = 31 * result + limit.hashCode() 98 | result = 31 * result + capacity.hashCode() 99 | result = 31 * result + byteOrder.hashCode() 100 | result = 31 * result + bytePointer.hashCode() 101 | return result 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/appleMain/kotlin/com/ditchoom/buffer/MutableDataBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import kotlinx.cinterop.ByteVar 4 | import kotlinx.cinterop.CPointer 5 | import kotlinx.cinterop.UnsafeNumber 6 | import kotlinx.cinterop.addressOf 7 | import kotlinx.cinterop.convert 8 | import kotlinx.cinterop.set 9 | import kotlinx.cinterop.usePinned 10 | import platform.CoreFoundation.CFDataRef 11 | import platform.Foundation.CFBridgingRelease 12 | import platform.Foundation.CFBridgingRetain 13 | import platform.Foundation.NSData 14 | import platform.Foundation.NSMakeRange 15 | import platform.Foundation.NSMutableData 16 | import platform.Foundation.NSString 17 | import platform.Foundation.dataUsingEncoding 18 | import platform.Foundation.dataWithBytesNoCopy 19 | import platform.Foundation.replaceBytesInRange 20 | import platform.Foundation.subdataWithRange 21 | 22 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, UnsafeNumber::class) 23 | class MutableDataBuffer( 24 | dataRef: NSData, 25 | override val byteOrder: ByteOrder, 26 | private val backingArray: ByteArray? = null, 27 | ) : DataBuffer(dataRef, byteOrder), PlatformBuffer { 28 | val mutableData = dataRef as? NSMutableData 29 | 30 | @Suppress("UNCHECKED_CAST") 31 | private val bytePointer = mutableData?.mutableBytes as? CPointer 32 | 33 | init { 34 | check( 35 | (bytePointer != null && backingArray == null) || 36 | (bytePointer == null && backingArray != null), 37 | ) 38 | } 39 | 40 | private fun writeByteInternal( 41 | index: Int, 42 | byte: Byte, 43 | ) { 44 | backingArray?.set(index, byte) 45 | bytePointer?.set(index, byte) 46 | } 47 | 48 | override fun resetForWrite() { 49 | position = 0 50 | limit = data.length.toInt() 51 | } 52 | 53 | override fun writeByte(byte: Byte): WriteBuffer { 54 | writeByteInternal(position++, byte) 55 | return this 56 | } 57 | 58 | override fun set( 59 | index: Int, 60 | byte: Byte, 61 | ): WriteBuffer { 62 | writeByteInternal(index, byte) 63 | return this 64 | } 65 | 66 | override fun writeBytes( 67 | bytes: ByteArray, 68 | offset: Int, 69 | length: Int, 70 | ): WriteBuffer { 71 | if (length < 1) { 72 | return this 73 | } 74 | if (mutableData != null) { 75 | val range = NSMakeRange(position.convert(), length.convert()) 76 | bytes.usePinned { pin -> 77 | mutableData.replaceBytesInRange(range, pin.addressOf(offset)) 78 | } 79 | position += length 80 | } else if (backingArray != null) { 81 | bytes.copyInto(backingArray, position, offset, offset + length) 82 | position += length 83 | } 84 | return this 85 | } 86 | 87 | override fun write(buffer: ReadBuffer) { 88 | if (buffer is DataBuffer && mutableData != null) { 89 | val bytesToCopySize = buffer.remaining() 90 | val otherSubdata = 91 | buffer.data.subdataWithRange( 92 | NSMakeRange( 93 | buffer.position().convert(), 94 | bytesToCopySize.convert(), 95 | ), 96 | ) 97 | mutableData.replaceBytesInRange( 98 | NSMakeRange( 99 | position.convert(), 100 | bytesToCopySize.convert(), 101 | ), 102 | otherSubdata.bytes, 103 | ) 104 | position += bytesToCopySize 105 | buffer.position(buffer.position() + bytesToCopySize) 106 | } else { 107 | val remainingByteArray = buffer.readByteArray(buffer.remaining()) 108 | writeBytes(remainingByteArray) 109 | } 110 | } 111 | 112 | override fun writeString( 113 | text: CharSequence, 114 | charset: Charset, 115 | ): WriteBuffer { 116 | val string = 117 | if (text is String) { 118 | text as NSString 119 | } else { 120 | @Suppress("CAST_NEVER_SUCCEEDS") 121 | text.toString() as NSString 122 | } 123 | val charsetEncoding = charset.toEncoding() 124 | write(DataBuffer(string.dataUsingEncoding(charsetEncoding)!!, byteOrder)) 125 | return this 126 | } 127 | 128 | companion object { 129 | fun wrap( 130 | byteArray: ByteArray, 131 | byteOrder: ByteOrder, 132 | ) = byteArray.useNSDataRef { 133 | MutableDataBuffer(it, byteOrder, byteArray) 134 | } 135 | } 136 | } 137 | 138 | private fun ByteArray.useNSDataRef(block: (NSData) -> T): T { 139 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class, UnsafeNumber::class) 140 | return usePinned { pin -> 141 | val bytesPointer = 142 | when { 143 | isNotEmpty() -> pin.addressOf(0) 144 | else -> null 145 | } 146 | val nsData = 147 | NSData.dataWithBytesNoCopy( 148 | bytes = bytesPointer, 149 | length = size.convert(), 150 | freeWhenDone = false, 151 | ) 152 | 153 | @Suppress("UNCHECKED_CAST") 154 | val typeRef = CFBridgingRetain(nsData) as CFDataRef 155 | 156 | try { 157 | block(nsData) 158 | } finally { 159 | CFBridgingRelease(typeRef) 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/AllocationZone.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | sealed class AllocationZone { 4 | /** Allocates a buffer in the java heap.*/ 5 | object Heap : AllocationZone() 6 | 7 | /** Allocates a direct buffer in native memory.*/ 8 | object Direct : AllocationZone() 9 | 10 | /** 11 | * Allocates a direct buffer in native shared memory. 12 | * In Javascript it will allocate a SharedArrayBuffer 13 | * Requires Android API 27, Oreo MR1. Otherwise defaults to [Direct]. 14 | ***/ 15 | object SharedMemory : AllocationZone() 16 | 17 | class Custom(val allocator: (Int) -> PlatformBuffer) : AllocationZone() 18 | } 19 | 20 | @Deprecated("Use SharedMemory") 21 | typealias AndroidSharedMemory = AllocationZone.SharedMemory 22 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/BufferFactory.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import kotlin.math.roundToInt 4 | 5 | expect fun PlatformBuffer.Companion.allocate( 6 | size: Int, 7 | zone: AllocationZone = AllocationZone.Heap, 8 | byteOrder: ByteOrder = ByteOrder.BIG_ENDIAN, 9 | ): PlatformBuffer 10 | 11 | expect fun PlatformBuffer.Companion.wrap( 12 | array: ByteArray, 13 | byteOrder: ByteOrder = ByteOrder.BIG_ENDIAN, 14 | ): PlatformBuffer 15 | 16 | @Deprecated( 17 | "Use toReadBuffer instead", 18 | ReplaceWith("toReadBuffer(Charset.UTF8, zone)", "com.ditchoom.buffer.Charset"), 19 | ) 20 | fun String.toBuffer(zone: AllocationZone = AllocationZone.Heap): ReadBuffer = toReadBuffer(Charset.UTF8, zone) 21 | 22 | fun CharSequence.maxBufferSize(charset: Charset): Int { 23 | return (charset.maxBytesPerChar * this.length).roundToInt() 24 | } 25 | 26 | fun String.toReadBuffer( 27 | charset: Charset = Charset.UTF8, 28 | zone: AllocationZone = AllocationZone.Heap, 29 | ): ReadBuffer { 30 | if (this == "") { 31 | return ReadBuffer.EMPTY_BUFFER 32 | } 33 | val maxBytes = maxBufferSize(charset) 34 | val buffer = PlatformBuffer.allocate(maxBytes, zone) 35 | buffer.writeString(this, charset) 36 | buffer.resetForRead() 37 | return buffer.slice() 38 | } 39 | 40 | fun CharSequence.utf8Length(): Int { 41 | var count = 0 42 | var i = 0 43 | val len = length 44 | while (i < len) { 45 | val ch = get(i) 46 | if (ch.code <= 0x7F) { 47 | count++ 48 | } else if (ch.code <= 0x7FF) { 49 | count += 2 50 | } else if (ch >= '\uD800' && ch.code < '\uDBFF'.code + 1) { 51 | count += 4 52 | ++i 53 | } else { 54 | count += 3 55 | } 56 | i++ 57 | } 58 | return count 59 | } 60 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/ByteOrder.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | enum class ByteOrder { 4 | BIG_ENDIAN, 5 | LITTLE_ENDIAN, 6 | } 7 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/Charset.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | enum class Charset(val averageBytesPerChar: Float, val maxBytesPerChar: Float) { 4 | UTF8(1.1f, 4f), 5 | UTF16(2f, 4f), 6 | UTF16BigEndian(2f, 4f), 7 | UTF16LittleEndian(2f, 4f), 8 | ASCII(1f, 1f), 9 | ISOLatin1(1f, 1f), // aka ISO/IEC 8859-1 10 | UTF32(4f, 4f), 11 | UTF32LittleEndian(4f, 4f), 12 | UTF32BigEndian(4f, 4f), 13 | } 14 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/FragmentedReadBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | /** 4 | * Provides a single ReadBuffer interface that delegates to multiple buffers. 5 | * While reading from a buffer sometimes you might need more data to complete the decoding operation. This class will 6 | * handle reading from multiple fragmented buffers in memory and provide a simple read api. 7 | */ 8 | class FragmentedReadBuffer( 9 | private val first: ReadBuffer, 10 | private val second: ReadBuffer, 11 | ) : ReadBuffer { 12 | private val firstInitialLimit = first.limit() 13 | private val secondInitialLimit = second.limit() 14 | private var currentPosition = 0 15 | private var currentLimit = firstInitialLimit + secondInitialLimit 16 | 17 | override val byteOrder: ByteOrder 18 | get() { 19 | throw IllegalStateException("Byte order is undefined for FragmentedReadBuffer") 20 | } 21 | 22 | override fun setLimit(limit: Int) { 23 | if (limit <= firstInitialLimit + secondInitialLimit) { 24 | currentLimit = limit 25 | } 26 | } 27 | 28 | override fun limit() = currentLimit 29 | 30 | override fun position() = currentPosition 31 | 32 | override fun position(newPosition: Int) { 33 | currentPosition = newPosition 34 | } 35 | 36 | override fun resetForRead() { 37 | currentPosition = 0 38 | first.resetForRead() 39 | second.resetForRead() 40 | } 41 | 42 | override fun readByte(): Byte { 43 | return if (currentPosition++ < firstInitialLimit) { 44 | first.readByte() 45 | } else { 46 | second.readByte() 47 | } 48 | } 49 | 50 | override fun get(index: Int): Byte { 51 | return if (currentPosition < firstInitialLimit) { 52 | first.readByte() 53 | } else { 54 | second.readByte() 55 | } 56 | } 57 | 58 | private fun readSizeIntoBuffer( 59 | size: Int, 60 | block: (ReadBuffer) -> T, 61 | ): T { 62 | val buffer = 63 | if (currentPosition < firstInitialLimit && currentPosition + size <= firstInitialLimit) { 64 | block(first) 65 | } else if (currentPosition < firstInitialLimit && currentPosition + size > firstInitialLimit) { 66 | val firstChunkSize = firstInitialLimit - currentPosition 67 | val secondChunkSize = size - firstChunkSize 68 | val secondBufferLimit = second.limit() 69 | second.setLimit(second.position() + secondChunkSize) 70 | val buffer = PlatformBuffer.allocate(size) 71 | buffer.write(first) 72 | buffer.write(second) 73 | second.setLimit(secondBufferLimit) 74 | buffer.resetForRead() 75 | block(buffer) 76 | } else { 77 | block(second) 78 | } 79 | currentPosition += size 80 | return buffer 81 | } 82 | 83 | override fun slice(): ReadBuffer { 84 | if (first.position() == 0 && first.limit() == 0) { 85 | return second 86 | } else if (second.position() == 0 && second.limit() == 0) { 87 | return first 88 | } 89 | val first = first.slice() 90 | val second = second.slice() 91 | val buffer = PlatformBuffer.allocate(first.limit() + second.limit()) 92 | buffer.write(first) 93 | buffer.write(second) 94 | buffer.resetForRead() 95 | return buffer 96 | } 97 | 98 | override fun readByteArray(size: Int): ByteArray { 99 | return readSizeIntoBuffer(size) { it.readByteArray(size) } 100 | } 101 | 102 | override fun readShort() = readSizeIntoBuffer(Short.SIZE_BYTES) { it.readShort() } 103 | 104 | override fun readInt() = readSizeIntoBuffer(Int.SIZE_BYTES) { it.readInt() } 105 | 106 | override fun readLong() = readSizeIntoBuffer(ULong.SIZE_BYTES) { it.readLong() } 107 | 108 | override fun readString( 109 | length: Int, 110 | charset: Charset, 111 | ): String { 112 | return readSizeIntoBuffer(length) { it.readString(length, charset) } 113 | } 114 | 115 | override fun readUtf8Line(): CharSequence { 116 | if (currentPosition < firstInitialLimit) { 117 | val initialFirstPosition = first.position() 118 | val firstUtf8 = first.readUtf8Line() 119 | val bytesRead = first.position() - initialFirstPosition 120 | return if (firstUtf8.toString().utf8Length().toLong() == bytesRead.toLong()) { 121 | // read the entire string, check the second one 122 | currentPosition = firstInitialLimit 123 | val secondInitialPosition = second.position() 124 | val secondLine = second.readUtf8Line() 125 | currentPosition += second.position() - secondInitialPosition 126 | StringBuilder(firstUtf8).append(secondLine) 127 | } else { 128 | firstUtf8 129 | } 130 | } else { 131 | val secondInitialPosition = second.position() 132 | val line = second.readUtf8Line() 133 | currentPosition += second.position() - secondInitialPosition 134 | return line 135 | } 136 | } 137 | 138 | fun getBuffers(out: MutableList) { 139 | if (first is FragmentedReadBuffer) { 140 | first.getBuffers(out) 141 | } else if (first is PlatformBuffer) { 142 | out += first 143 | } 144 | if (second is FragmentedReadBuffer) { 145 | second.getBuffers(out) 146 | } else if (second is PlatformBuffer) { 147 | out += second 148 | } 149 | } 150 | 151 | fun toSingleBuffer(zone: AllocationZone = AllocationZone.Heap): PlatformBuffer { 152 | val firstLimit = firstInitialLimit 153 | val secondLimit = secondInitialLimit 154 | val initialPosition = position() 155 | val buffer = PlatformBuffer.allocate(firstLimit + secondLimit - initialPosition, zone) 156 | walk { 157 | buffer.write(it) 158 | } 159 | buffer.position(initialPosition) 160 | return buffer 161 | } 162 | 163 | fun walk(visitor: (ReadBuffer) -> Unit) { 164 | if (first is FragmentedReadBuffer) { 165 | first.walk(visitor) 166 | } else { 167 | visitor(first) 168 | } 169 | if (second is FragmentedReadBuffer) { 170 | second.walk(visitor) 171 | } else { 172 | visitor(second) 173 | } 174 | } 175 | } 176 | 177 | fun List.toComposableBuffer(): ReadBuffer { 178 | if (isEmpty()) { 179 | return ReadBuffer.EMPTY_BUFFER 180 | } 181 | if (size == 1) { 182 | return first() 183 | } 184 | if (size == 2) { 185 | return FragmentedReadBuffer(first(), get(1)) 186 | } 187 | val half = size / 2 188 | return FragmentedReadBuffer( 189 | subList(0, half).toComposableBuffer(), 190 | subList(half, size).toComposableBuffer(), 191 | ) 192 | } 193 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/Parcelable.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 4 | expect interface Parcelable 5 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/PlatformBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | interface PlatformBuffer : ReadBuffer, WriteBuffer, SuspendCloseable, Parcelable { 4 | val capacity: Int 5 | 6 | companion object 7 | } 8 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/PositionBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | interface PositionBuffer { 4 | val byteOrder: ByteOrder 5 | 6 | fun setLimit(limit: Int) 7 | 8 | fun limit(): Int 9 | 10 | fun position(): Int 11 | 12 | fun position(newPosition: Int) 13 | 14 | fun remaining() = limit() - position() 15 | 16 | fun hasRemaining() = position() < limit() 17 | } 18 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/ReadBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | interface ReadBuffer : PositionBuffer { 4 | fun resetForRead() 5 | 6 | fun readByte(): Byte 7 | 8 | operator fun get(index: Int): Byte 9 | 10 | // slice does not change the position 11 | fun slice(): ReadBuffer 12 | 13 | fun readBytes(size: Int): ReadBuffer { 14 | if (size < 1) { 15 | return EMPTY_BUFFER 16 | } 17 | val oldLimit = limit() 18 | val oldPosition = position() 19 | setLimit(position() + size) 20 | val bytes = slice() 21 | position(oldPosition + size) 22 | setLimit(oldLimit) 23 | return bytes 24 | } 25 | 26 | fun readByteArray(size: Int): ByteArray 27 | 28 | fun readUnsignedByte(): UByte = readByte().toUByte() 29 | 30 | fun getUnsignedByte(index: Int): UByte = get(index).toUByte() 31 | 32 | fun readShort(): Short = readNumberWithByteSize(Short.SIZE_BYTES).toShort() 33 | 34 | fun getShort(index: Int): Short = getNumberWithStartIndexAndByteSize(index, Short.SIZE_BYTES).toShort() 35 | 36 | fun readUnsignedShort(): UShort = readShort().toUShort() 37 | 38 | fun getUnsignedShort(index: Int): UShort = getShort(index).toUShort() 39 | 40 | fun readInt(): Int = readNumberWithByteSize(Int.SIZE_BYTES).toInt() 41 | 42 | fun getInt(index: Int): Int = getNumberWithStartIndexAndByteSize(index, Int.SIZE_BYTES).toInt() 43 | 44 | fun readUnsignedInt(): UInt = readInt().toUInt() 45 | 46 | fun getUnsignedInt(index: Int): UInt = getInt(index).toUInt() 47 | 48 | fun readFloat(): Float = Float.fromBits(readInt()) 49 | 50 | fun getFloat(index: Int): Float = Float.fromBits(getInt(index)) 51 | 52 | fun readLong(): Long = readNumberWithByteSize(Long.SIZE_BYTES) 53 | 54 | fun getLong(index: Int): Long = getNumberWithStartIndexAndByteSize(index, Long.SIZE_BYTES) 55 | 56 | fun readUnsignedLong(): ULong = readLong().toULong() 57 | 58 | fun getUnsignedLong(index: Int): ULong = getLong(index).toULong() 59 | 60 | fun readDouble(): Double = Double.fromBits(readLong()) 61 | 62 | fun getDouble(index: Int): Double = Double.fromBits(getLong(index)) 63 | 64 | fun readString( 65 | length: Int, 66 | charset: Charset = Charset.UTF8, 67 | ): String 68 | 69 | @Deprecated( 70 | "Use readString instead", 71 | ReplaceWith("readString(bytes, Charset.UTF8)", "com.ditchoom.buffer.Charset"), 72 | ) 73 | fun readUtf8(bytes: UInt): CharSequence = readString(bytes.toInt(), Charset.UTF8) 74 | 75 | @Deprecated( 76 | "Use readString instead", 77 | ReplaceWith("readString(bytes, Charset.UTF8)", "com.ditchoom.buffer.Charset"), 78 | ) 79 | fun readUtf8(bytes: Int): CharSequence = readString(bytes, Charset.UTF8) 80 | 81 | fun readUtf8Line(): CharSequence { 82 | val initialPosition = position() 83 | var lastByte: Byte = 0 84 | var currentByte: Byte = 0 85 | var bytesRead = 0 86 | while (remaining() > 0) { 87 | lastByte = currentByte 88 | currentByte = readByte() 89 | bytesRead++ 90 | if (currentByte == newLine[1]) { 91 | break 92 | } 93 | } 94 | val carriageFeedPositionIncrement = 95 | if (lastByte == newLine[0] && currentByte == newLine[1]) { 96 | 2 97 | } else if (currentByte == newLine[1]) { 98 | 1 99 | } else { 100 | 0 101 | } 102 | 103 | val bytesToRead = bytesRead - carriageFeedPositionIncrement 104 | position(initialPosition) 105 | val result = readString(bytesToRead, Charset.UTF8) 106 | position(position() + carriageFeedPositionIncrement) 107 | return result 108 | } 109 | 110 | fun readNumberWithByteSize(numberOfBytes: Int): Long { 111 | check(numberOfBytes in 1..8) { "byte size out of range" } 112 | val byteSizeRange = 113 | when (byteOrder) { 114 | ByteOrder.LITTLE_ENDIAN -> 0 until numberOfBytes 115 | ByteOrder.BIG_ENDIAN -> numberOfBytes - 1 downTo 0 116 | } 117 | var number = 0L 118 | for (i in byteSizeRange) { 119 | val bitIndex = i * 8 120 | number = readByte().toLong() and 0xff shl bitIndex or number 121 | } 122 | return number 123 | } 124 | 125 | fun getNumberWithStartIndexAndByteSize( 126 | startIndex: Int, 127 | numberOfBytes: Int, 128 | ): Long { 129 | check(numberOfBytes in 1..8) { "byte size out of range" } 130 | val byteSizeRange = 131 | when (byteOrder) { 132 | ByteOrder.LITTLE_ENDIAN -> 0 until numberOfBytes 133 | ByteOrder.BIG_ENDIAN -> numberOfBytes - 1 downTo 0 134 | } 135 | var number = 0L 136 | var index = startIndex 137 | for (i in byteSizeRange) { 138 | val bitIndex = i * 8 139 | number = get(index++).toLong() and 0xff shl bitIndex or number 140 | } 141 | return number 142 | } 143 | 144 | companion object { 145 | val newLine = "\r\n".encodeToByteArray() 146 | val EMPTY_BUFFER = PlatformBuffer.allocate(0) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/SuspendCloseable.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | interface SuspendCloseable { 4 | suspend fun close() 5 | } 6 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/TransformedReadBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | class TransformedReadBuffer(val origin: ReadBuffer, val transformer: ((Int, Byte) -> Byte)) : 4 | ReadBuffer { 5 | override val byteOrder: ByteOrder = origin.byteOrder 6 | 7 | override fun limit() = origin.limit() 8 | 9 | override fun setLimit(limit: Int) { 10 | origin.setLimit(limit) 11 | } 12 | 13 | override fun position() = origin.position() 14 | 15 | override fun position(newPosition: Int) = origin.position(newPosition) 16 | 17 | override fun resetForRead() = origin.resetForRead() 18 | 19 | override fun readByte() = transformer(position(), origin.readByte()) 20 | 21 | override fun get(index: Int): Byte = transformer(position(), origin[index]) 22 | 23 | override fun slice(): ReadBuffer = origin.slice() 24 | 25 | override fun readByteArray(size: Int): ByteArray { 26 | val byteArray = origin.readByteArray(size) 27 | for (position in byteArray.indices) { 28 | byteArray[position] = transformer(position, byteArray[position]) 29 | } 30 | return byteArray 31 | } 32 | 33 | override fun readShort(): Short { 34 | val buffer = PlatformBuffer.allocate(Short.SIZE_BYTES) 35 | buffer.writeShort(origin.readShort()) 36 | buffer.resetForRead() 37 | val byte1 = transformer(position(), buffer.readByte()) 38 | val byte2 = transformer(position(), buffer.readByte()) 39 | buffer.resetForWrite() 40 | buffer.writeByte(byte1) 41 | buffer.writeByte(byte2) 42 | buffer.resetForRead() 43 | return buffer.readShort() 44 | } 45 | 46 | override fun getShort(index: Int): Short { 47 | val buffer = PlatformBuffer.allocate(Short.SIZE_BYTES) 48 | buffer.writeShort(origin.getShort(index)) 49 | buffer.resetForRead() 50 | val byte1 = transformer(position(), buffer.readByte()) 51 | val byte2 = transformer(position(), buffer.readByte()) 52 | buffer.resetForWrite() 53 | buffer.writeByte(byte1) 54 | buffer.writeByte(byte2) 55 | buffer.resetForRead() 56 | return buffer.readShort() 57 | } 58 | 59 | override fun readInt(): Int { 60 | val buffer = PlatformBuffer.allocate(Int.SIZE_BYTES) 61 | buffer.writeInt(origin.readInt()) 62 | buffer.resetForRead() 63 | val byte1 = transformer(position(), buffer.readByte()) 64 | val byte2 = transformer(position(), buffer.readByte()) 65 | val byte3 = transformer(position(), buffer.readByte()) 66 | val byte4 = transformer(position(), buffer.readByte()) 67 | buffer.resetForWrite() 68 | buffer.writeByte(byte1) 69 | buffer.writeByte(byte2) 70 | buffer.writeByte(byte3) 71 | buffer.writeByte(byte4) 72 | buffer.resetForRead() 73 | return buffer.readInt() 74 | } 75 | 76 | override fun getInt(index: Int): Int { 77 | val buffer = PlatformBuffer.allocate(Int.SIZE_BYTES) 78 | buffer.writeInt(origin.getInt(index)) 79 | buffer.resetForRead() 80 | val byte1 = transformer(position(), buffer.readByte()) 81 | val byte2 = transformer(position(), buffer.readByte()) 82 | val byte3 = transformer(position(), buffer.readByte()) 83 | val byte4 = transformer(position(), buffer.readByte()) 84 | buffer.resetForWrite() 85 | buffer.writeByte(byte1) 86 | buffer.writeByte(byte2) 87 | buffer.writeByte(byte3) 88 | buffer.writeByte(byte4) 89 | buffer.resetForRead() 90 | return buffer.readInt() 91 | } 92 | 93 | override fun readLong(): Long { 94 | val buffer = PlatformBuffer.allocate(Long.SIZE_BYTES) 95 | buffer.writeLong(origin.readLong()) 96 | buffer.resetForRead() 97 | val byte1 = transformer(position(), buffer.readByte()) 98 | val byte2 = transformer(position(), buffer.readByte()) 99 | val byte3 = transformer(position(), buffer.readByte()) 100 | val byte4 = transformer(position(), buffer.readByte()) 101 | val byte5 = transformer(position(), buffer.readByte()) 102 | val byte6 = transformer(position(), buffer.readByte()) 103 | val byte7 = transformer(position(), buffer.readByte()) 104 | val byte8 = transformer(position(), buffer.readByte()) 105 | buffer.resetForWrite() 106 | buffer.writeByte(byte1) 107 | buffer.writeByte(byte2) 108 | buffer.writeByte(byte3) 109 | buffer.writeByte(byte4) 110 | buffer.writeByte(byte5) 111 | buffer.writeByte(byte6) 112 | buffer.writeByte(byte7) 113 | buffer.writeByte(byte8) 114 | buffer.resetForRead() 115 | return buffer.readLong() 116 | } 117 | 118 | override fun getLong(index: Int): Long { 119 | val buffer = PlatformBuffer.allocate(Long.SIZE_BYTES) 120 | buffer.writeLong(origin.getLong(index)) 121 | buffer.resetForRead() 122 | val byte1 = transformer(position(), buffer.readByte()) 123 | val byte2 = transformer(position(), buffer.readByte()) 124 | val byte3 = transformer(position(), buffer.readByte()) 125 | val byte4 = transformer(position(), buffer.readByte()) 126 | val byte5 = transformer(position(), buffer.readByte()) 127 | val byte6 = transformer(position(), buffer.readByte()) 128 | val byte7 = transformer(position(), buffer.readByte()) 129 | val byte8 = transformer(position(), buffer.readByte()) 130 | buffer.resetForWrite() 131 | buffer.writeByte(byte1) 132 | buffer.writeByte(byte2) 133 | buffer.writeByte(byte3) 134 | buffer.writeByte(byte4) 135 | buffer.writeByte(byte5) 136 | buffer.writeByte(byte6) 137 | buffer.writeByte(byte7) 138 | buffer.writeByte(byte8) 139 | buffer.resetForRead() 140 | return buffer.readLong() 141 | } 142 | 143 | override fun readString( 144 | length: Int, 145 | charset: Charset, 146 | ): String { 147 | return when (charset) { 148 | Charset.UTF8 -> readByteArray(length).decodeToString() 149 | else -> throw UnsupportedOperationException("Unsupported charset $charset") 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/ditchoom/buffer/WriteBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | interface WriteBuffer : PositionBuffer { 4 | fun resetForWrite() 5 | 6 | fun writeByte(byte: Byte): WriteBuffer 7 | 8 | operator fun set( 9 | index: Int, 10 | byte: Byte, 11 | ): WriteBuffer 12 | 13 | @Deprecated( 14 | "Use writeByte for explicitness. This will be removed in the next release", 15 | ReplaceWith("writeByte(byte)"), 16 | ) 17 | fun write(byte: Byte): WriteBuffer = writeByte(byte) 18 | 19 | fun writeBytes(bytes: ByteArray): WriteBuffer = writeBytes(bytes, 0, bytes.size) 20 | 21 | @Deprecated( 22 | "Use writeBytes for explicitness. This will be removed in the next release", 23 | ReplaceWith("writeBytes(bytes)"), 24 | ) 25 | fun write(bytes: ByteArray): WriteBuffer = writeBytes(bytes) 26 | 27 | fun writeBytes( 28 | bytes: ByteArray, 29 | offset: Int, 30 | length: Int, 31 | ): WriteBuffer 32 | 33 | @Deprecated( 34 | "Use writeBytes for explicitness. This will be removed in the next release", 35 | ReplaceWith("writeBytes(bytes, offset, length)"), 36 | ) 37 | fun write( 38 | bytes: ByteArray, 39 | offset: Int, 40 | length: Int, 41 | ): WriteBuffer = writeBytes(bytes, offset, length) 42 | 43 | fun writeUByte(uByte: UByte): WriteBuffer = writeByte(uByte.toByte()) 44 | 45 | operator fun set( 46 | index: Int, 47 | uByte: UByte, 48 | ) = set(index, uByte.toByte()) 49 | 50 | @Deprecated( 51 | "Use writeUByte for explicitness. This will be removed in the next release", 52 | ReplaceWith("writeUByte(uByte)"), 53 | ) 54 | fun write(uByte: UByte): WriteBuffer = writeUByte(uByte) 55 | 56 | fun writeShort(short: Short): WriteBuffer = writeNumberOfByteSize(short.toLong(), Short.SIZE_BYTES) 57 | 58 | operator fun set( 59 | index: Int, 60 | short: Short, 61 | ) = setIndexNumberAndByteSize(index, short.toLong(), Short.SIZE_BYTES) 62 | 63 | @Deprecated( 64 | "Use writeShort for explicitness. This will be removed in the next release", 65 | ReplaceWith("writeShort(short)"), 66 | ) 67 | fun write(short: Short): WriteBuffer = writeUShort(short.toUShort()) 68 | 69 | fun writeUShort(uShort: UShort): WriteBuffer = writeShort(uShort.toShort()) 70 | 71 | operator fun set( 72 | index: Int, 73 | uShort: UShort, 74 | ) = set(index, uShort.toShort()) 75 | 76 | @Deprecated( 77 | "Use writeUShort for explicitness. This will be removed in the next release", 78 | ReplaceWith("writeUShort(uShort)"), 79 | ) 80 | fun write(uShort: UShort): WriteBuffer = writeUShort(uShort) 81 | 82 | fun writeInt(int: Int): WriteBuffer = writeNumberOfByteSize(int.toLong(), Int.SIZE_BYTES) 83 | 84 | operator fun set( 85 | index: Int, 86 | int: Int, 87 | ) = setIndexNumberAndByteSize(index, int.toLong(), Int.SIZE_BYTES) 88 | 89 | @Deprecated( 90 | "Use writeInt for explicitness. This will be removed in the next release", 91 | ReplaceWith("writeInt(int)"), 92 | ) 93 | fun write(int: Int): WriteBuffer = writeInt(int) 94 | 95 | fun writeUInt(uInt: UInt): WriteBuffer = writeInt(uInt.toInt()) 96 | 97 | operator fun set( 98 | index: Int, 99 | uInt: UInt, 100 | ) = set(index, uInt.toInt()) 101 | 102 | @Deprecated( 103 | "Use writeUInt for explicitness. This will be removed in the next release", 104 | ReplaceWith("writeUInt(uInt)"), 105 | ) 106 | fun write(uInt: UInt): WriteBuffer = writeUInt(uInt) 107 | 108 | fun writeLong(long: Long): WriteBuffer = writeNumberOfByteSize(long, Long.SIZE_BYTES) 109 | 110 | operator fun set( 111 | index: Int, 112 | long: Long, 113 | ) = setIndexNumberAndByteSize(index, long, Long.SIZE_BYTES) 114 | 115 | fun writeNumberOfByteSize( 116 | number: Long, 117 | byteSize: Int, 118 | ): WriteBuffer { 119 | check(byteSize in 1..8) { "byte size out of range" } 120 | val byteSizeRange = 121 | when (byteOrder) { 122 | ByteOrder.LITTLE_ENDIAN -> 0 until byteSize 123 | ByteOrder.BIG_ENDIAN -> byteSize - 1 downTo 0 124 | } 125 | for (i in byteSizeRange) { 126 | val bitIndex = i * 8 127 | writeByte((number shr bitIndex and 0xff).toByte()) 128 | } 129 | return this 130 | } 131 | 132 | fun setIndexNumberAndByteSize( 133 | index: Int, 134 | number: Long, 135 | byteSize: Int, 136 | ): WriteBuffer { 137 | check(byteSize in 1..8) { "byte size out of range" } 138 | val p = position() 139 | position(index) 140 | writeNumberOfByteSize(number, byteSize) 141 | position(p) 142 | return this 143 | } 144 | 145 | @Deprecated( 146 | "Use writeLong for explicitness. This will be removed in the next release", 147 | ReplaceWith("writeLong(long)"), 148 | ) 149 | fun write(long: Long): WriteBuffer = writeLong(long) 150 | 151 | fun writeULong(uLong: ULong): WriteBuffer = writeLong(uLong.toLong()) 152 | 153 | operator fun set( 154 | index: Int, 155 | uLong: ULong, 156 | ) = set(index, uLong.toLong()) 157 | 158 | @Deprecated( 159 | "Use writeULong for explicitness. This will be removed in the next release", 160 | ReplaceWith("writeULong(uLong)"), 161 | ) 162 | fun write(uLong: ULong): WriteBuffer = writeULong(uLong) 163 | 164 | fun writeFloat(float: Float): WriteBuffer = writeInt(float.toRawBits()) 165 | 166 | operator fun set( 167 | index: Int, 168 | float: Float, 169 | ) = set(index, float.toRawBits()) 170 | 171 | @Deprecated( 172 | "Use writeFloat for explicitness. This will be removed in the next release", 173 | ReplaceWith("writeFloat(float)"), 174 | ) 175 | fun write(float: Float): WriteBuffer = writeFloat(float) 176 | 177 | fun writeDouble(double: Double): WriteBuffer = writeLong(double.toRawBits()) 178 | 179 | operator fun set( 180 | index: Int, 181 | double: Double, 182 | ) = set(index, double.toRawBits()) 183 | 184 | @Deprecated( 185 | "Use writeDouble for explicitness. This will be removed in the next release", 186 | ReplaceWith("writeDouble(double)"), 187 | ) 188 | fun write(double: Double): WriteBuffer = writeDouble(double) 189 | 190 | @Deprecated( 191 | "Use writeString(txt, Charset.UTF8) instead", 192 | ReplaceWith("writeString(text, Charset.UTF8)", "com.ditchoom.buffer.Charset"), 193 | ) 194 | fun writeUtf8(text: CharSequence): WriteBuffer = writeString(text, Charset.UTF8) 195 | 196 | fun writeString( 197 | text: CharSequence, 198 | charset: Charset = Charset.UTF8, 199 | ): WriteBuffer 200 | 201 | fun write(buffer: ReadBuffer) 202 | } 203 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/ditchoom/buffer/BufferTests.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import kotlin.math.absoluteValue 4 | import kotlin.math.ceil 5 | import kotlin.test.Test 6 | import kotlin.test.assertContentEquals 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertFails 9 | import kotlin.test.assertFalse 10 | import kotlin.test.assertTrue 11 | 12 | class BufferTests { 13 | @Test 14 | fun slice() { 15 | val platformBuffer = PlatformBuffer.allocate(3) 16 | platformBuffer.writeByte((-1).toByte()) 17 | platformBuffer.resetForRead() 18 | val slicedBuffer = platformBuffer.slice() 19 | assertEquals(0, platformBuffer.position()) 20 | assertEquals(1, platformBuffer.limit()) 21 | assertEquals(0, slicedBuffer.position()) 22 | assertEquals(1, slicedBuffer.limit()) 23 | assertEquals(-1, slicedBuffer.readByte()) 24 | assertEquals(1, slicedBuffer.position()) 25 | assertEquals(1, slicedBuffer.limit()) 26 | } 27 | 28 | @Test 29 | fun sharedMemoryAllocates() { 30 | val platformBuffer = 31 | PlatformBuffer.allocate( 32 | Byte.SIZE_BYTES + UByte.SIZE_BYTES + 33 | Short.SIZE_BYTES + UShort.SIZE_BYTES + 34 | Int.SIZE_BYTES + UInt.SIZE_BYTES + 35 | Long.SIZE_BYTES + ULong.SIZE_BYTES, 36 | AllocationZone.SharedMemory, 37 | ) 38 | testAllPrimitives(platformBuffer) 39 | } 40 | 41 | @Test 42 | fun absolute() { 43 | val platformBuffer = 44 | PlatformBuffer.allocate( 45 | Byte.SIZE_BYTES + UByte.SIZE_BYTES + 46 | Short.SIZE_BYTES + UShort.SIZE_BYTES + 47 | Int.SIZE_BYTES + UInt.SIZE_BYTES + 48 | Long.SIZE_BYTES + ULong.SIZE_BYTES, 49 | ) 50 | testAllPrimitives(platformBuffer) 51 | } 52 | 53 | private fun testAllPrimitives(platformBuffer: PlatformBuffer) { 54 | var index = 0 55 | platformBuffer[index] = Byte.MIN_VALUE 56 | index += Byte.SIZE_BYTES 57 | platformBuffer[index] = UByte.MAX_VALUE 58 | index += UByte.SIZE_BYTES 59 | 60 | platformBuffer[index] = Short.MIN_VALUE 61 | index += Short.SIZE_BYTES 62 | platformBuffer[index] = UShort.MAX_VALUE 63 | index += UShort.SIZE_BYTES 64 | 65 | platformBuffer[index] = Int.MIN_VALUE 66 | index += Int.SIZE_BYTES 67 | platformBuffer[index] = UInt.MAX_VALUE 68 | index += UInt.SIZE_BYTES 69 | 70 | platformBuffer[index] = Long.MIN_VALUE 71 | index += Long.SIZE_BYTES 72 | platformBuffer[index] = ULong.MAX_VALUE 73 | 74 | index = 0 75 | assertEquals(platformBuffer[index], Byte.MIN_VALUE, "absolute byte read") 76 | index += Byte.SIZE_BYTES 77 | assertEquals(platformBuffer.getUnsignedByte(index), UByte.MAX_VALUE, "absolute ubyte read") 78 | index += UByte.SIZE_BYTES 79 | 80 | assertEquals(platformBuffer.getShort(index), Short.MIN_VALUE, "absolute short read") 81 | index += Short.SIZE_BYTES 82 | assertEquals(platformBuffer.getUnsignedShort(index), UShort.MAX_VALUE, "absolute ushort read") 83 | index += UShort.SIZE_BYTES 84 | 85 | assertEquals(platformBuffer.getInt(index), Int.MIN_VALUE, "absolute int read") 86 | index += Int.SIZE_BYTES 87 | assertEquals(platformBuffer.getUnsignedInt(index), UInt.MAX_VALUE, "absolute uint read") 88 | index += UInt.SIZE_BYTES 89 | 90 | assertEquals(platformBuffer.getLong(index), Long.MIN_VALUE, "absolute long read") 91 | index += Long.SIZE_BYTES 92 | assertEquals(platformBuffer.getUnsignedLong(index), ULong.MAX_VALUE, "absolute ulong read") 93 | 94 | // double validate with relative reading 95 | assertEquals(0, platformBuffer.position(), "relative initial position") 96 | assertEquals(platformBuffer.readByte(), Byte.MIN_VALUE, "relative byte read") 97 | assertEquals(1, platformBuffer.position(), "relative after byte read position") 98 | assertEquals(platformBuffer.readUnsignedByte(), UByte.MAX_VALUE, "relative ubyte read") 99 | assertEquals(2, platformBuffer.position(), "relative after ubyte read position") 100 | 101 | assertEquals(platformBuffer.readShort(), Short.MIN_VALUE, "relative short read") 102 | assertEquals(4, platformBuffer.position(), "relative after short read position") 103 | assertEquals(platformBuffer.readUnsignedShort(), UShort.MAX_VALUE, "relative ushort read") 104 | assertEquals(6, platformBuffer.position(), "relative after ushort read position") 105 | 106 | assertEquals(platformBuffer.readInt(), Int.MIN_VALUE, "relative int read") 107 | assertEquals(10, platformBuffer.position(), "relative after int read position") 108 | assertEquals(platformBuffer.readUnsignedInt(), UInt.MAX_VALUE, "relative uint read") 109 | assertEquals(14, platformBuffer.position(), "relative after uint read position") 110 | 111 | assertEquals(platformBuffer.readLong(), Long.MIN_VALUE, "relative long read") 112 | assertEquals(22, platformBuffer.position(), "relative after long read position") 113 | assertEquals(platformBuffer.readUnsignedLong(), ULong.MAX_VALUE, "relative ulong read") 114 | assertEquals(30, platformBuffer.position(), "relative after ulong read position") 115 | } 116 | 117 | @Test 118 | fun readByte() { 119 | val platformBuffer = PlatformBuffer.allocate(3) 120 | platformBuffer.writeByte((-1).toByte()) 121 | platformBuffer.resetForRead() 122 | val slicedBuffer = platformBuffer.readBytes(1) 123 | assertEquals(1, platformBuffer.position()) 124 | assertEquals(1, platformBuffer.limit()) 125 | assertEquals(0, slicedBuffer.position()) 126 | assertEquals(1, slicedBuffer.limit()) 127 | assertEquals(-1, slicedBuffer.readByte()) 128 | assertEquals(1, slicedBuffer.position()) 129 | assertEquals(1, slicedBuffer.limit()) 130 | } 131 | 132 | @Test 133 | fun getByte() { 134 | val platformBuffer = PlatformBuffer.allocate(3) 135 | val expectedValue = (-1).toByte() 136 | platformBuffer[0] = expectedValue 137 | val valueRead = platformBuffer[0] 138 | assertEquals(0, platformBuffer.position()) 139 | assertEquals(3, platformBuffer.limit()) 140 | assertEquals(-1, valueRead) 141 | } 142 | 143 | @Test 144 | fun sliceAndReadUtf8() { 145 | val expected = "test" 146 | // the first two bytes are not visible characters 147 | val bytes = 148 | byteArrayOf( 149 | -126, 150 | 4, 151 | expected[0].code.toByte(), 152 | expected[1].code.toByte(), 153 | expected[2].code.toByte(), 154 | expected[3].code.toByte(), 155 | ) 156 | val platformBuffer = PlatformBuffer.allocate(bytes.size) 157 | platformBuffer.writeBytes(bytes) 158 | platformBuffer.position(2) 159 | assertEquals(expected, platformBuffer.readString(4, Charset.UTF8)) 160 | } 161 | 162 | @Test 163 | fun sliceFragmented() { 164 | val platformBuffer1 = PlatformBuffer.allocate(3) 165 | platformBuffer1.writeByte(1.toByte()) 166 | platformBuffer1.resetForRead() 167 | 168 | val platformBuffer2 = PlatformBuffer.allocate(3) 169 | platformBuffer2.writeByte((-1).toByte()) 170 | platformBuffer2.resetForRead() 171 | 172 | val fragmentedBuffer = FragmentedReadBuffer(platformBuffer1, platformBuffer2) 173 | assertEquals(0, fragmentedBuffer.position()) 174 | assertEquals(2, fragmentedBuffer.limit()) 175 | assertEquals(1, fragmentedBuffer.readByte()) 176 | assertEquals(1, fragmentedBuffer.position()) 177 | assertEquals(2, fragmentedBuffer.limit()) 178 | assertEquals(-1, fragmentedBuffer.readByte()) 179 | assertEquals(2, fragmentedBuffer.position()) 180 | assertEquals(2, fragmentedBuffer.limit()) 181 | fragmentedBuffer.resetForRead() 182 | assertEquals(0, fragmentedBuffer.position()) 183 | assertEquals(2, fragmentedBuffer.limit()) 184 | val slicedBuffer = fragmentedBuffer.slice() 185 | assertEquals(0, slicedBuffer.position()) 186 | assertEquals(2, slicedBuffer.limit()) 187 | assertEquals(1, slicedBuffer.readByte()) 188 | assertEquals(1, slicedBuffer.position()) 189 | assertEquals(2, slicedBuffer.limit()) 190 | assertEquals(-1, slicedBuffer.readByte()) 191 | assertEquals(2, slicedBuffer.position()) 192 | assertEquals(2, slicedBuffer.limit()) 193 | } 194 | 195 | @Test 196 | fun byte() { 197 | val platformBuffer = PlatformBuffer.allocate(1) 198 | val byte = (-1).toByte() 199 | platformBuffer.writeByte(byte) 200 | platformBuffer.resetForRead() 201 | assertEquals(byte.toInt(), platformBuffer.readByte().toInt()) 202 | } 203 | 204 | @Test 205 | fun byteArray() { 206 | val size = 200 207 | val platformBuffer = PlatformBuffer.allocate(size) 208 | val bytes = ByteArray(size) { -1 } 209 | platformBuffer.writeBytes(bytes) 210 | platformBuffer.resetForRead() 211 | val byteArray = platformBuffer.readByteArray(size) 212 | assertEquals(bytes.count(), byteArray.count()) 213 | bytes.forEachIndexed { index, byte -> 214 | assertEquals(byte, byteArray[index], "Index $index") 215 | } 216 | } 217 | 218 | @Test 219 | fun relativeUnsignedByte() { 220 | val platformBuffer = PlatformBuffer.allocate(1) 221 | val byte = (-1).toUByte() 222 | platformBuffer.writeUByte(byte) 223 | platformBuffer.resetForRead() 224 | assertEquals(byte.toInt(), platformBuffer.readUnsignedByte().toInt()) 225 | assertFalse(platformBuffer.hasRemaining()) 226 | } 227 | 228 | @Test 229 | fun absoluteUnsignedByte() { 230 | val platformBuffer = PlatformBuffer.allocate(1) 231 | val byte = (-1).toUByte() 232 | platformBuffer[0] = byte 233 | assertEquals(byte, platformBuffer[0].toUByte()) 234 | assertEquals(1, platformBuffer.remaining()) 235 | } 236 | 237 | @Test 238 | fun relativeUnsignedShort() { 239 | val platformBuffer = PlatformBuffer.allocate(2) 240 | val uShort = UShort.MAX_VALUE.toInt() / 2 241 | platformBuffer.writeUShort(uShort.toUShort()) 242 | platformBuffer.resetForRead() 243 | assertEquals(uShort, platformBuffer.readUnsignedShort().toInt()) 244 | platformBuffer.resetForRead() 245 | val msb = platformBuffer.readByte() 246 | val lsb = platformBuffer.readByte() 247 | val value = 248 | ( 249 | (0xff and msb.toInt() shl 8) 250 | or (0xff and lsb.toInt() shl 0) 251 | ).toUShort() 252 | assertEquals(value.toInt(), uShort) 253 | assertEquals(0, platformBuffer.remaining()) 254 | } 255 | 256 | @Test 257 | fun absoluteUnsignedShort() { 258 | val platformBuffer = PlatformBuffer.allocate(2) 259 | val uShort = UShort.MAX_VALUE 260 | platformBuffer[0] = uShort 261 | assertEquals(uShort, platformBuffer.getUnsignedShort(0)) 262 | val msb = platformBuffer[0] 263 | val lsb = platformBuffer[1] 264 | val value = 265 | ( 266 | (0xff and msb.toInt() shl 8) 267 | or (0xff and lsb.toInt() shl 0) 268 | ).toUShort() 269 | assertEquals(value, uShort) 270 | assertEquals(2, platformBuffer.remaining()) 271 | } 272 | 273 | @Test 274 | fun allUShortValues() { 275 | val buffer = PlatformBuffer.allocate(UShort.MAX_VALUE.toInt() * UShort.SIZE_BYTES) 276 | (0 until UShort.MAX_VALUE.toInt()).forEach { 277 | buffer.writeUShort(it.toUShort()) 278 | } 279 | buffer.resetForRead() 280 | (0 until UShort.MAX_VALUE.toInt()).forEach { 281 | assertEquals(it, buffer.readUnsignedShort().toInt()) 282 | } 283 | } 284 | 285 | @Test 286 | fun relativeUnsignedShortHalf() { 287 | val platformBuffer = PlatformBuffer.allocate(2) 288 | val uShort = (UShort.MAX_VALUE / 2u).toUShort() 289 | platformBuffer.writeUShort(uShort) 290 | platformBuffer.resetForRead() 291 | val actual = platformBuffer.readUnsignedShort().toInt() 292 | assertEquals(uShort.toInt(), actual) 293 | assertEquals(uShort.toString(), actual.toString()) 294 | assertEquals(0, platformBuffer.remaining()) 295 | } 296 | 297 | @Test 298 | fun absoluteUnsignedShortHalf() { 299 | val platformBuffer = PlatformBuffer.allocate(UShort.SIZE_BYTES) 300 | val uShort = (UShort.MAX_VALUE / 2u).toUShort() 301 | platformBuffer[0] = uShort 302 | val actual = platformBuffer.getUnsignedShort(0) 303 | assertEquals(uShort, actual) 304 | assertEquals(uShort.toString(), actual.toString()) 305 | assertEquals(0, platformBuffer.position()) 306 | } 307 | 308 | @Test 309 | fun relativeUnsignedInt() { 310 | val platformBuffer = PlatformBuffer.allocate(4) 311 | val uInt = (-1).toUInt() 312 | platformBuffer.writeUInt(uInt) 313 | platformBuffer.resetForRead() 314 | assertEquals(uInt.toLong(), platformBuffer.readUnsignedInt().toLong()) 315 | assertEquals(0, platformBuffer.remaining()) 316 | } 317 | 318 | @Test 319 | fun absoluteUnsignedInt() { 320 | val platformBuffer = PlatformBuffer.allocate(4) 321 | val uInt = (-1).toUInt() 322 | platformBuffer[0] = uInt 323 | assertEquals(uInt.toLong(), platformBuffer.getUnsignedInt(0).toLong()) 324 | assertEquals(4, platformBuffer.remaining()) 325 | } 326 | 327 | @Test 328 | fun unsignedIntHalf() { 329 | val platformBuffer = PlatformBuffer.allocate(4) 330 | val uInt = Int.MAX_VALUE.toUInt() / 2u 331 | platformBuffer.writeUInt(uInt) 332 | platformBuffer.resetForRead() 333 | assertEquals(uInt.toLong(), platformBuffer.readUnsignedInt().toLong()) 334 | assertEquals(0, platformBuffer.remaining()) 335 | } 336 | 337 | @Test 338 | fun relativeLong() { 339 | val platformBuffer = PlatformBuffer.allocate(Long.SIZE_BYTES) 340 | val long = (1234).toLong() 341 | assertEquals(0, platformBuffer.position()) 342 | platformBuffer.writeLong(long) 343 | assertEquals(Long.SIZE_BYTES, platformBuffer.position()) 344 | platformBuffer.resetForRead() 345 | assertEquals(0, platformBuffer.position()) 346 | assertEquals(long, platformBuffer.readLong()) 347 | assertEquals(Long.SIZE_BYTES, platformBuffer.position()) 348 | platformBuffer.resetForRead() 349 | assertEquals(0, platformBuffer.position()) 350 | assertEquals(long, platformBuffer.readNumberWithByteSize(Long.SIZE_BYTES)) 351 | assertEquals(Long.SIZE_BYTES, platformBuffer.position()) 352 | 353 | val platformBufferLittleEndian = 354 | PlatformBuffer.allocate(Long.SIZE_BYTES, byteOrder = ByteOrder.LITTLE_ENDIAN) 355 | platformBufferLittleEndian.writeLong(long) 356 | platformBufferLittleEndian.resetForRead() 357 | assertEquals(long, platformBufferLittleEndian.readLong()) 358 | platformBufferLittleEndian.resetForRead() 359 | assertEquals(long, platformBufferLittleEndian.readNumberWithByteSize(Long.SIZE_BYTES)) 360 | } 361 | 362 | @Test 363 | fun absoluteLong() { 364 | val platformBuffer = PlatformBuffer.allocate(Long.SIZE_BYTES) 365 | val long = 12345L 366 | assertEquals(0, platformBuffer.position()) 367 | platformBuffer[0] = long 368 | assertEquals(0, platformBuffer.position()) 369 | assertEquals( 370 | long, 371 | platformBuffer.getLong(0), 372 | "getLong BIG_ENDIAN buffer[" + 373 | "${platformBuffer[0]}, ${platformBuffer[1]}, ${platformBuffer[2]}, ${platformBuffer[3]}, " + 374 | "${platformBuffer[4]}, ${platformBuffer[5]}, ${platformBuffer[6]}, ${platformBuffer[7]}]", 375 | ) 376 | assertEquals(0, platformBuffer.position()) 377 | assertEquals( 378 | long, 379 | platformBuffer.getNumberWithStartIndexAndByteSize(0, Long.SIZE_BYTES), 380 | "getNumberWithStartIndexAndByteSize", 381 | ) 382 | assertEquals(0, platformBuffer.position()) 383 | 384 | val platformBufferLE = 385 | PlatformBuffer.allocate(Long.SIZE_BYTES, byteOrder = ByteOrder.LITTLE_ENDIAN) 386 | platformBufferLE[0] = long 387 | assertEquals(0, platformBufferLE.position()) 388 | assertEquals( 389 | long, 390 | platformBufferLE.getLong(0), 391 | "getLong LITTLE_ENDIAN buffer[" + 392 | "${platformBufferLE[0]}, ${platformBufferLE[1]}, ${platformBufferLE[2]}, ${platformBufferLE[3]}, " + 393 | "${platformBufferLE[4]}, ${platformBufferLE[5]}, ${platformBufferLE[6]}, ${platformBufferLE[7]}]", 394 | ) 395 | assertEquals(0, platformBufferLE.position()) 396 | assertEquals( 397 | long, 398 | platformBufferLE.getNumberWithStartIndexAndByteSize(0, Long.SIZE_BYTES), 399 | "getNumberWithStartIndexAndByteSizeLittleEndian", 400 | ) 401 | assertEquals(0, platformBufferLE.position()) 402 | } 403 | 404 | @Test 405 | fun relativeLongBits() { 406 | val platformBuffer = PlatformBuffer.allocate(Long.SIZE_BYTES) 407 | val long = (1234).toLong() 408 | platformBuffer.writeNumberOfByteSize(long, Long.SIZE_BYTES) 409 | platformBuffer.resetForRead() 410 | assertEquals(long, platformBuffer.readLong()) 411 | platformBuffer.resetForRead() 412 | assertEquals(long, platformBuffer.readNumberWithByteSize(Long.SIZE_BYTES)) 413 | 414 | val platformBufferLittleEndian = 415 | PlatformBuffer.allocate(Long.SIZE_BYTES, byteOrder = ByteOrder.LITTLE_ENDIAN) 416 | platformBufferLittleEndian.writeNumberOfByteSize(long, Long.SIZE_BYTES) 417 | platformBufferLittleEndian.resetForRead() 418 | assertEquals(long, platformBufferLittleEndian.readLong()) 419 | platformBufferLittleEndian.resetForRead() 420 | assertEquals(long, platformBufferLittleEndian.readNumberWithByteSize(Long.SIZE_BYTES)) 421 | } 422 | 423 | @Test 424 | fun absoluteLongBits() { 425 | val platformBuffer = PlatformBuffer.allocate(Long.SIZE_BYTES) 426 | val long = (1234).toLong() 427 | platformBuffer.setIndexNumberAndByteSize(0, long, Long.SIZE_BYTES) 428 | assertEquals(0, platformBuffer.position()) 429 | assertEquals(long, platformBuffer.getLong(0)) 430 | assertEquals(0, platformBuffer.position()) 431 | assertEquals(long, platformBuffer.getNumberWithStartIndexAndByteSize(0, Long.SIZE_BYTES)) 432 | assertEquals(0, platformBuffer.position()) 433 | 434 | val platformBufferLittleEndian = 435 | PlatformBuffer.allocate(Long.SIZE_BYTES, byteOrder = ByteOrder.LITTLE_ENDIAN) 436 | platformBufferLittleEndian.setIndexNumberAndByteSize(0, long, Long.SIZE_BYTES) 437 | assertEquals(0, platformBufferLittleEndian.position()) 438 | assertEquals(long, platformBufferLittleEndian.getLong(0)) 439 | assertEquals(0, platformBufferLittleEndian.position()) 440 | assertEquals(long, platformBufferLittleEndian.getNumberWithStartIndexAndByteSize(0, Long.SIZE_BYTES)) 441 | assertEquals(0, platformBufferLittleEndian.position()) 442 | } 443 | 444 | @Test 445 | fun realtiveFloat() { 446 | val platformBuffer = PlatformBuffer.allocate(Float.SIZE_BYTES) 447 | val float = 123.456f 448 | platformBuffer.writeFloat(float) 449 | platformBuffer.resetForRead() 450 | // Note that in Kotlin/JS Float range is wider than "single format" bit layout can represent, 451 | // so some Float values may overflow, underflow or lose their accuracy after conversion to bits and back. 452 | assertTrue { (float - platformBuffer.readFloat()).absoluteValue < 0.00001f } 453 | } 454 | 455 | @Test 456 | fun absoluteFloat() { 457 | val platformBuffer = PlatformBuffer.allocate(Float.SIZE_BYTES) 458 | val float = 123.456f 459 | platformBuffer[0] = float 460 | assertEquals(0, platformBuffer.position()) 461 | // Note that in Kotlin/JS Float range is wider than "single format" bit layout can represent, 462 | // so some Float values may overflow, underflow or lose their accuracy after conversion to bits and back. 463 | assertTrue { (float - platformBuffer.getFloat(0)).absoluteValue < 0.00001f } 464 | assertEquals(0, platformBuffer.position()) 465 | } 466 | 467 | @Test 468 | fun relativeDouble() { 469 | val platformBuffer = PlatformBuffer.allocate(Double.SIZE_BYTES) 470 | val double = 123.456 471 | platformBuffer.writeDouble(double) 472 | platformBuffer.resetForRead() 473 | assertEquals(double, platformBuffer.readDouble()) 474 | } 475 | 476 | @Test 477 | fun absoluteDouble() { 478 | val platformBuffer = PlatformBuffer.allocate(Double.SIZE_BYTES) 479 | val double = 123.456 480 | assertEquals(0, platformBuffer.position()) 481 | platformBuffer[0] = double 482 | assertEquals(0, platformBuffer.position()) 483 | assertEquals( 484 | double, 485 | platformBuffer.getDouble(0), 486 | "getDouble BIG_ENDIAN buffer[" + 487 | "${platformBuffer[0]}, ${platformBuffer[1]}, ${platformBuffer[2]}, ${platformBuffer[3]}, " + 488 | "${platformBuffer[4]}, ${platformBuffer[5]}, ${platformBuffer[6]}, ${platformBuffer[7]}]", 489 | ) 490 | assertEquals(0, platformBuffer.position()) 491 | 492 | val platformBufferLE = 493 | PlatformBuffer.allocate(Long.SIZE_BYTES, byteOrder = ByteOrder.LITTLE_ENDIAN) 494 | platformBufferLE[0] = double 495 | assertEquals(0, platformBufferLE.position()) 496 | assertEquals( 497 | double, 498 | platformBufferLE.getDouble(0), 499 | "getDouble LITTLE_ENDIAN buffer[" + 500 | "${platformBufferLE[0]}, ${platformBufferLE[1]}, ${platformBufferLE[2]}, ${platformBufferLE[3]}, " + 501 | "${platformBufferLE[4]}, ${platformBufferLE[5]}, ${platformBufferLE[6]}, ${platformBufferLE[7]}]", 502 | ) 503 | assertEquals(0, platformBufferLE.position()) 504 | } 505 | 506 | @Test 507 | fun positionWriteBytes() { 508 | val text = "Hello world!" 509 | val input = PlatformBuffer.wrap(text.encodeToByteArray()) 510 | val output = PlatformBuffer.allocate(text.length) 511 | output.write(input) 512 | assertEquals(input.position(), text.length) 513 | assertEquals(output.position(), text.length) 514 | output.position(0) 515 | assertEquals(output.readString(text.length), text) 516 | } 517 | 518 | @Test 519 | fun utf8String() { 520 | val string = "yolo swag lyfestyle" 521 | assertEquals(19, string.utf8Length()) 522 | val platformBuffer = PlatformBuffer.allocate(19) 523 | platformBuffer.writeString(string, Charset.UTF8) 524 | platformBuffer.resetForRead() 525 | val actual = platformBuffer.readString(19, Charset.UTF8) 526 | assertEquals(string.length, actual.length) 527 | assertEquals(string, actual) 528 | } 529 | 530 | @Test 531 | fun readUtf8LineSingle() { 532 | val text = "hello" 533 | val buffer = text.toReadBuffer(Charset.UTF8) 534 | assertEquals("hello", buffer.readUtf8Line().toString()) 535 | assertEquals(buffer.remaining(), 0) 536 | } 537 | 538 | @Test 539 | fun readUtf8LineDouble() { 540 | val text = "hello\r\n" 541 | val buffer = text.toReadBuffer(Charset.UTF8) 542 | assertEquals("hello", buffer.readUtf8Line().toString()) 543 | assertEquals("", buffer.readUtf8Line().toString()) 544 | assertEquals(buffer.remaining(), 0) 545 | } 546 | 547 | @Test 548 | fun readUtf8LineStart() { 549 | val text = "\r\nhello" 550 | val buffer = text.toReadBuffer(Charset.UTF8) 551 | assertEquals("", buffer.readUtf8Line().toString()) 552 | assertEquals("hello", buffer.readUtf8Line().toString()) 553 | assertEquals(buffer.remaining(), 0) 554 | } 555 | 556 | @Test 557 | fun readUtf8LineStartN() { 558 | val text = "\nhello" 559 | val buffer = text.toReadBuffer(Charset.UTF8) 560 | assertEquals("", buffer.readUtf8Line().toString()) 561 | assertEquals("hello", buffer.readUtf8Line().toString()) 562 | assertEquals(buffer.remaining(), 0) 563 | } 564 | 565 | @Test 566 | fun readUtf8LineMix() { 567 | val text = "\nhello\r\nhello\nhello\r\n" 568 | val buffer = text.toReadBuffer(Charset.UTF8) 569 | assertEquals("", buffer.readUtf8Line().toString()) 570 | assertEquals("hello", buffer.readUtf8Line().toString()) 571 | assertEquals("hello", buffer.readUtf8Line().toString()) 572 | assertEquals("hello", buffer.readUtf8Line().toString()) 573 | assertEquals("", buffer.readUtf8Line().toString()) 574 | assertEquals(buffer.remaining(), 0) 575 | } 576 | 577 | @Test 578 | fun readUtf8LineMixMulti() { 579 | val text = "\nhello\r\n\nhello\n\nhello\r\n" 580 | val buffer = text.toReadBuffer(Charset.UTF8) 581 | assertEquals("", buffer.readUtf8Line().toString()) 582 | assertEquals("hello", buffer.readUtf8Line().toString()) 583 | assertEquals("", buffer.readUtf8Line().toString()) 584 | assertEquals("hello", buffer.readUtf8Line().toString()) 585 | assertEquals("", buffer.readUtf8Line().toString()) 586 | assertEquals("hello", buffer.readUtf8Line().toString()) 587 | assertEquals("", buffer.readUtf8Line().toString()) 588 | assertEquals(buffer.remaining(), 0) 589 | } 590 | 591 | @Test 592 | fun readUtf8Line() { 593 | val stringArray = "yolo swag lyfestyle".split(' ') 594 | assertEquals(3, stringArray.size) 595 | val newLineString = stringArray.joinToString("\r\n") 596 | val stringBuffer = newLineString.toReadBuffer(Charset.UTF8) 597 | stringArray.forEach { 598 | val line = stringBuffer.readUtf8Line() 599 | assertEquals(it, line.toString()) 600 | } 601 | } 602 | 603 | @Test 604 | fun readByteArray() { 605 | val string = "yolo swag lyfestyle" 606 | val stringBuffer = string.toReadBuffer(Charset.UTF8) 607 | assertEquals(string[0], Char(stringBuffer.readByte().toInt())) 608 | val s = stringBuffer.readByteArray(stringBuffer.remaining()) 609 | assertEquals(string.substring(1), s.decodeToString()) 610 | } 611 | 612 | @Test 613 | fun readByteArraySizeZeroDoesNotCrash() { 614 | val string = "yolo swag lyfestyle" 615 | val stringBuffer = string.toReadBuffer(Charset.UTF8) 616 | val emptyByteArray = stringBuffer.readByteArray(0) 617 | assertContentEquals(emptyByteArray, ByteArray(0)) 618 | } 619 | 620 | @Test 621 | fun endianWrite() { 622 | val littleEndian2 = PlatformBuffer.allocate(2, byteOrder = ByteOrder.LITTLE_ENDIAN) 623 | littleEndian2.writeShort(0x0102.toShort()) 624 | littleEndian2.resetForRead() 625 | assertEquals(0x02u, littleEndian2.readUnsignedByte()) 626 | assertEquals(0x01u, littleEndian2.readUnsignedByte()) 627 | 628 | val bigEndian2 = PlatformBuffer.allocate(2, byteOrder = ByteOrder.BIG_ENDIAN) 629 | bigEndian2.writeShort(0x0102.toShort()) 630 | bigEndian2.resetForRead() 631 | assertEquals(0x01u, bigEndian2.readUnsignedByte()) 632 | assertEquals(0x02u, bigEndian2.readUnsignedByte()) 633 | 634 | val littleEndian4 = PlatformBuffer.allocate(4, byteOrder = ByteOrder.LITTLE_ENDIAN) 635 | littleEndian4.writeInt(0x01020304) 636 | littleEndian4.resetForRead() 637 | assertEquals(0x04u, littleEndian4.readUnsignedByte()) 638 | assertEquals(0x03u, littleEndian4.readUnsignedByte()) 639 | assertEquals(0x02u, littleEndian4.readUnsignedByte()) 640 | assertEquals(0x01u, littleEndian4.readUnsignedByte()) 641 | 642 | val bigEndian4 = PlatformBuffer.allocate(4, byteOrder = ByteOrder.BIG_ENDIAN) 643 | bigEndian4.writeInt(0x01020304) 644 | bigEndian4.resetForRead() 645 | assertEquals(0x01u, bigEndian4.readUnsignedByte()) 646 | assertEquals(0x02u, bigEndian4.readUnsignedByte()) 647 | assertEquals(0x03u, bigEndian4.readUnsignedByte()) 648 | assertEquals(0x04u, bigEndian4.readUnsignedByte()) 649 | 650 | val littleEndian8 = PlatformBuffer.allocate(8, byteOrder = ByteOrder.LITTLE_ENDIAN) 651 | littleEndian8.writeLong(0x0102030405060708) 652 | littleEndian8.resetForRead() 653 | assertEquals(0x08u, littleEndian8.readUnsignedByte()) 654 | assertEquals(0x07u, littleEndian8.readUnsignedByte()) 655 | assertEquals(0x06u, littleEndian8.readUnsignedByte()) 656 | assertEquals(0x05u, littleEndian8.readUnsignedByte()) 657 | assertEquals(0x04u, littleEndian8.readUnsignedByte()) 658 | assertEquals(0x03u, littleEndian8.readUnsignedByte()) 659 | assertEquals(0x02u, littleEndian8.readUnsignedByte()) 660 | assertEquals(0x01u, littleEndian8.readUnsignedByte()) 661 | 662 | val bigEndian8 = PlatformBuffer.allocate(8, byteOrder = ByteOrder.BIG_ENDIAN) 663 | bigEndian8.writeLong(0x0102030405060708) 664 | bigEndian8.resetForRead() 665 | assertEquals(0x01u, bigEndian8.readUnsignedByte()) 666 | assertEquals(0x02u, bigEndian8.readUnsignedByte()) 667 | assertEquals(0x03u, bigEndian8.readUnsignedByte()) 668 | assertEquals(0x04u, bigEndian8.readUnsignedByte()) 669 | assertEquals(0x05u, bigEndian8.readUnsignedByte()) 670 | assertEquals(0x06u, bigEndian8.readUnsignedByte()) 671 | assertEquals(0x07u, bigEndian8.readUnsignedByte()) 672 | assertEquals(0x08u, bigEndian8.readUnsignedByte()) 673 | } 674 | 675 | @Test 676 | fun endianRead() { 677 | val littleEndian2 = PlatformBuffer.allocate(2, byteOrder = ByteOrder.LITTLE_ENDIAN) 678 | littleEndian2.writeByte(0x01.toByte()) 679 | littleEndian2.writeByte(0x02.toByte()) 680 | littleEndian2.resetForRead() 681 | assertEquals(0x0201.toShort(), littleEndian2.readShort()) 682 | 683 | val bigEndian2 = PlatformBuffer.allocate(2, byteOrder = ByteOrder.BIG_ENDIAN) 684 | bigEndian2.writeByte(0x01.toByte()) 685 | bigEndian2.writeByte(0x02.toByte()) 686 | bigEndian2.resetForRead() 687 | assertEquals(0x0102.toShort(), bigEndian2.readShort()) 688 | 689 | val littleEndian4 = PlatformBuffer.allocate(4, byteOrder = ByteOrder.LITTLE_ENDIAN) 690 | littleEndian4.writeByte(0x01.toByte()) 691 | littleEndian4.writeByte(0x02.toByte()) 692 | littleEndian4.writeByte(0x03.toByte()) 693 | littleEndian4.writeByte(0x04.toByte()) 694 | littleEndian4.resetForRead() 695 | assertEquals(0x04030201, littleEndian4.readInt()) 696 | 697 | val bigEndian4 = PlatformBuffer.allocate(4, byteOrder = ByteOrder.BIG_ENDIAN) 698 | bigEndian4.writeByte(0x01.toByte()) 699 | bigEndian4.writeByte(0x02.toByte()) 700 | bigEndian4.writeByte(0x03.toByte()) 701 | bigEndian4.writeByte(0x04.toByte()) 702 | bigEndian4.resetForRead() 703 | assertEquals(0x01020304, bigEndian4.readInt()) 704 | 705 | val littleEndian8 = PlatformBuffer.allocate(8, byteOrder = ByteOrder.LITTLE_ENDIAN) 706 | littleEndian8.writeByte(0x01.toByte()) 707 | littleEndian8.writeByte(0x02.toByte()) 708 | littleEndian8.writeByte(0x03.toByte()) 709 | littleEndian8.writeByte(0x04.toByte()) 710 | littleEndian8.writeByte(0x05.toByte()) 711 | littleEndian8.writeByte(0x06.toByte()) 712 | littleEndian8.writeByte(0x07.toByte()) 713 | littleEndian8.writeByte(0x08.toByte()) 714 | littleEndian8.resetForRead() 715 | assertEquals(0x0807060504030201, littleEndian8.readLong()) 716 | 717 | val bigEndian8 = PlatformBuffer.allocate(8, byteOrder = ByteOrder.BIG_ENDIAN) 718 | bigEndian8.writeByte(0x01.toByte()) 719 | bigEndian8.writeByte(0x02.toByte()) 720 | bigEndian8.writeByte(0x03.toByte()) 721 | bigEndian8.writeByte(0x04.toByte()) 722 | bigEndian8.writeByte(0x05.toByte()) 723 | bigEndian8.writeByte(0x06.toByte()) 724 | bigEndian8.writeByte(0x07.toByte()) 725 | bigEndian8.writeByte(0x08.toByte()) 726 | bigEndian8.resetForRead() 727 | assertEquals(0x0102030405060708, bigEndian8.readLong()) 728 | } 729 | 730 | @Test 731 | fun partialByteArray() { 732 | val byteArray = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 733 | val partialArray = byteArray.sliceArray(2..6) 734 | val buffer = PlatformBuffer.allocate(byteArray.size) 735 | val position = buffer.position() 736 | buffer.writeBytes(byteArray, 2, 5) 737 | val deltaPosition = buffer.position() - position 738 | assertEquals(5, deltaPosition) 739 | buffer.resetForRead() 740 | assertContentEquals(partialArray, buffer.readByteArray(5)) 741 | } 742 | 743 | @Test 744 | fun wrap() { 745 | val byteArray = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 746 | val buffer = PlatformBuffer.wrap(byteArray) 747 | assertEquals(byteArray.size, buffer.remaining(), "remaining") 748 | assertContentEquals(byteArray, buffer.readByteArray(buffer.remaining()), "equals") 749 | buffer.resetForRead() 750 | assertEquals(byteArray.size, buffer.remaining(), "remaining") 751 | byteArray.fill(-1) 752 | val modified = byteArray.copyOf() 753 | assertContentEquals(buffer.readByteArray(buffer.remaining()), modified, "modify original") 754 | } 755 | 756 | @Test 757 | fun encoding() { 758 | val string = "yolo swag lyfestyle" 759 | var successfulCount = 0 760 | Charset.values().forEach { 761 | val stringBuffer = PlatformBuffer.allocate(80) 762 | try { 763 | stringBuffer.writeString(string, it) 764 | stringBuffer.resetForRead() 765 | assertEquals( 766 | string, 767 | stringBuffer.readString(stringBuffer.remaining(), it), 768 | it.toString(), 769 | ) 770 | successfulCount++ 771 | } catch (e: UnsupportedOperationException) { 772 | // unallowed type. 773 | } 774 | } 775 | assertTrue { successfulCount > 0 } 776 | } 777 | 778 | @Test 779 | fun invalidCharacterInBufferThrows() { 780 | assertFails { 781 | val buffer = PlatformBuffer.wrap(byteArrayOf(2, 126, 33, -66, -100, 4, -39, 108)) 782 | buffer.readString(buffer.remaining()) 783 | println("should have failed by now") 784 | } 785 | } 786 | 787 | @Test 788 | fun emptyString() { 789 | val s = "".toReadBuffer() 790 | assertEquals(0, s.position()) 791 | assertEquals(0, s.limit()) 792 | } 793 | 794 | @OptIn(ExperimentalStdlibApi::class) 795 | @Test 796 | fun complexReadWrapReadAgain1() { 797 | val str = "0001A960DBD8A500006500006400010A003132333435363738" 798 | 799 | @OptIn(ExperimentalStdlibApi::class) 800 | fun String.toByteArrayFromHex(): ByteArray { 801 | var value = this 802 | if (value.isEmpty()) return byteArrayOf() 803 | val length = ceil(value.length / 2.0).toInt() 804 | value = value.padStart(length * 2, '0') 805 | return value.hexToByteArray() 806 | } 807 | val hex = str.toByteArrayFromHex() 808 | assertContentEquals( 809 | hex, 810 | byteArrayOf(0, 1, -87, 96, -37, -40, -91, 0, 0, 101, 0, 0, 100, 0, 1, 10, 0, 49, 50, 51, 52, 53, 54, 55, 56), 811 | ) 812 | val buf = PlatformBuffer.wrap(hex) 813 | assertBufferEquals( 814 | buf, 815 | byteArrayOf(0, 1, -87, 96, -37, -40, -91, 0, 0, 101, 0, 0, 100, 0, 1, 10, 0, 49, 50, 51, 52, 53, 54, 55, 56), 816 | ) 817 | assertEquals(0, buf.position()) 818 | assertEquals(25, buf.limit()) 819 | val messageId = buf.readUnsignedShort() 820 | assertBufferEquals(buf, byteArrayOf(-87, 96, -37, -40, -91, 0, 0, 101, 0, 0, 100, 0, 1, 10, 0, 49, 50, 51, 52, 53, 54, 55, 56)) 821 | assertEquals(messageId.toInt(), 1) 822 | assertEquals(2, buf.position()) 823 | assertEquals(25, buf.limit()) 824 | val cmd = buf.readByte() 825 | assertBufferEquals(buf, byteArrayOf(96, -37, -40, -91, 0, 0, 101, 0, 0, 100, 0, 1, 10, 0, 49, 50, 51, 52, 53, 54, 55, 56)) 826 | assertEquals(-87, cmd) 827 | assertEquals(3, buf.position()) 828 | assertEquals(25, buf.limit()) 829 | val rem = buf.remaining() 830 | assertEquals(22, rem) 831 | val data = buf.readByteArray(rem) 832 | assertContentEquals(data, byteArrayOf(96, -37, -40, -91, 0, 0, 101, 0, 0, 100, 0, 1, 10, 0, 49, 50, 51, 52, 53, 54, 55, 56)) 833 | val newBuffer = PlatformBuffer.wrap(data) 834 | assertEquals(0, newBuffer.position()) 835 | assertEquals(22, newBuffer.limit()) 836 | assertBufferEquals(newBuffer, byteArrayOf(96, -37, -40, -91, 0, 0, 101, 0, 0, 100, 0, 1, 10, 0, 49, 50, 51, 52, 53, 54, 55, 56)) 837 | val b = newBuffer.readByteArray(4) 838 | assertEquals(b.toHexString().uppercase(), "60DBD8A5") 839 | } 840 | 841 | fun assertBufferEquals( 842 | b: ReadBuffer, 843 | byteArray: ByteArray, 844 | ) { 845 | val p = b.position() 846 | val l = b.limit() 847 | assertContentEquals(b.readByteArray(b.remaining()), byteArray) 848 | b.position(p) 849 | b.setLimit(l) 850 | } 851 | 852 | @Test 853 | fun wrapByteArraySharesBuffer() { 854 | val array = byteArrayOf(0, 1, 2, 3, 4, 5) 855 | val buf = PlatformBuffer.wrap(array) 856 | assertEquals(1, buf[1]) 857 | array[1] = -1 858 | assertEquals(-1, buf[1]) 859 | } 860 | 861 | @Test 862 | fun simpleReadWrapReadAgain() { 863 | val array = byteArrayOf(0, 1, 2, 3, 4, 5) 864 | val buf = PlatformBuffer.wrap(array) 865 | buf.readBytes(3) 866 | val bytesRead = buf.readByteArray(buf.remaining()) 867 | assertEquals(buf.position(), 6) 868 | assertEquals(buf.limit(), 6) 869 | assertEquals(bytesRead.size, 3) 870 | val buffer2 = PlatformBuffer.wrap(bytesRead) 871 | assertBufferEquals(buffer2, byteArrayOf(3, 4, 5)) 872 | } 873 | } 874 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/ditchoom/buffer/FragmentedReadBufferTests.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS") 2 | 3 | package com.ditchoom.buffer 4 | 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertTrue 8 | 9 | class FragmentedReadBufferTests { 10 | @Test 11 | fun readByteFromFirstBuffer() { 12 | val expectedFirstByte = Byte.MAX_VALUE 13 | val first = PlatformBuffer.allocate(1) 14 | first.writeByte(expectedFirstByte) 15 | first.resetForRead() 16 | val second = PlatformBuffer.allocate(1) 17 | val expectedSecondByte = Byte.MIN_VALUE 18 | second.writeByte(expectedSecondByte) 19 | second.resetForRead() 20 | 21 | val composableBuffer = FragmentedReadBuffer(first, second) 22 | assertEquals(expectedFirstByte, composableBuffer.readByte()) 23 | } 24 | 25 | @Test 26 | fun readByteFromSecondBuffer() { 27 | val expectedFirstByte = Byte.MAX_VALUE 28 | val first = PlatformBuffer.allocate(1) 29 | first.writeByte(expectedFirstByte) 30 | first.resetForRead() 31 | val second = PlatformBuffer.allocate(1) 32 | val expectedSecondByte = Byte.MIN_VALUE 33 | second.writeByte(expectedSecondByte) 34 | second.resetForRead() 35 | 36 | val composableBuffer = FragmentedReadBuffer(first, second) 37 | composableBuffer.position(1) 38 | assertEquals(expectedSecondByte, composableBuffer.readByte()) 39 | } 40 | 41 | @Test 42 | fun readShort() { 43 | val first = PlatformBuffer.allocate(Short.SIZE_BYTES) 44 | first.writeShort(1.toShort()) 45 | first.resetForRead() 46 | val second = PlatformBuffer.allocate(Short.SIZE_BYTES) 47 | second.writeShort(2.toShort()) 48 | second.resetForRead() 49 | val composableBuffer = FragmentedReadBuffer(first, second) 50 | assertEquals(1, composableBuffer.readShort()) 51 | assertEquals(2, composableBuffer.readShort()) 52 | } 53 | 54 | @Test 55 | fun readInt() { 56 | val first = PlatformBuffer.allocate(Int.SIZE_BYTES) 57 | first.writeInt(1) 58 | first.resetForRead() 59 | val second = PlatformBuffer.allocate(Int.SIZE_BYTES) 60 | second.writeInt(2) 61 | second.resetForRead() 62 | val composableBuffer = FragmentedReadBuffer(first, second) 63 | assertEquals(1, composableBuffer.readInt()) 64 | assertEquals(2, composableBuffer.readInt()) 65 | } 66 | 67 | @Test 68 | fun readLong() { 69 | val first = PlatformBuffer.allocate(Long.SIZE_BYTES) 70 | first.writeLong(1L) 71 | first.resetForRead() 72 | val second = PlatformBuffer.allocate(Long.SIZE_BYTES) 73 | second.writeLong(2L) 74 | second.resetForRead() 75 | val composableBuffer = FragmentedReadBuffer(first, second) 76 | assertEquals(1L, composableBuffer.readLong()) 77 | assertEquals(2L, composableBuffer.readLong()) 78 | } 79 | 80 | @Test 81 | fun readBytesFromThreeBuffers() { 82 | val expectedFirstByte = Byte.MAX_VALUE 83 | val first = PlatformBuffer.allocate(1) 84 | first.writeByte(expectedFirstByte) 85 | first.resetForRead() 86 | val second = PlatformBuffer.allocate(1) 87 | val expectedSecondByte = 6.toByte() 88 | second.writeByte(expectedSecondByte) 89 | second.resetForRead() 90 | val third = PlatformBuffer.allocate(1) 91 | val expectedThirdByte = Byte.MIN_VALUE 92 | third.writeByte(expectedThirdByte) 93 | third.resetForRead() 94 | 95 | val composableBuffer = arrayListOf(first, second, third).toComposableBuffer() 96 | assertEquals(expectedFirstByte, composableBuffer.readByte()) 97 | assertEquals(expectedSecondByte, composableBuffer.readByte()) 98 | assertEquals(expectedThirdByte, composableBuffer.readByte()) 99 | } 100 | 101 | @Test 102 | fun readBytesFromFourBuffers() { 103 | val expectedFirstByte = Byte.MAX_VALUE 104 | val first = PlatformBuffer.allocate(1) 105 | first.writeByte(expectedFirstByte) 106 | first.resetForRead() 107 | val second = PlatformBuffer.allocate(1) 108 | val expectedSecondByte = 6.toByte() 109 | second.writeByte(expectedSecondByte) 110 | second.resetForRead() 111 | val third = PlatformBuffer.allocate(1) 112 | val expectedThirdByte = 12.toByte() 113 | third.writeByte(expectedThirdByte) 114 | third.resetForRead() 115 | 116 | val fourth = PlatformBuffer.allocate(1) 117 | val expectedFourthByte = Byte.MIN_VALUE 118 | fourth.writeByte(expectedFourthByte) 119 | fourth.resetForRead() 120 | 121 | val composableBuffer = arrayListOf(first, second, third, fourth).toComposableBuffer() 122 | assertEquals(expectedFirstByte, composableBuffer.readByte()) 123 | assertEquals(expectedSecondByte, composableBuffer.readByte()) 124 | assertEquals(expectedThirdByte, composableBuffer.readByte()) 125 | assertEquals(expectedFourthByte, composableBuffer.readByte()) 126 | } 127 | 128 | @Test 129 | fun readBytesFromFiveBuffers() { 130 | val expectedFirstByte = Byte.MAX_VALUE 131 | val first = PlatformBuffer.allocate(1) 132 | first.writeByte(expectedFirstByte) 133 | first.resetForRead() 134 | val second = PlatformBuffer.allocate(1) 135 | val expectedSecondByte = 6.toByte() 136 | second.writeByte(expectedSecondByte) 137 | second.resetForRead() 138 | val third = PlatformBuffer.allocate(1) 139 | val expectedThirdByte = (-1).toByte() 140 | third.writeByte(expectedThirdByte) 141 | third.resetForRead() 142 | 143 | val fourth = PlatformBuffer.allocate(1) 144 | val expectedFourthByte = 0.toByte() 145 | fourth.writeByte(expectedFourthByte) 146 | fourth.resetForRead() 147 | 148 | val fifth = PlatformBuffer.allocate(1) 149 | val expectedFifthByte = Byte.MIN_VALUE 150 | fifth.writeByte(expectedFifthByte) 151 | fifth.resetForRead() 152 | 153 | val composableBuffer = arrayListOf(first, second, third, fourth, fifth).toComposableBuffer() 154 | assertEquals(expectedFirstByte, composableBuffer.readByte()) 155 | assertEquals(expectedSecondByte, composableBuffer.readByte()) 156 | assertEquals(expectedThirdByte, composableBuffer.readByte()) 157 | assertEquals(expectedFourthByte, composableBuffer.readByte()) 158 | assertEquals(expectedFifthByte, composableBuffer.readByte()) 159 | } 160 | 161 | @Test 162 | fun readUByteFromFirstBuffer() { 163 | val expectedFirstUByte = UByte.MAX_VALUE 164 | val first = PlatformBuffer.allocate(1) 165 | first.writeUByte(expectedFirstUByte) 166 | first.resetForRead() 167 | val second = PlatformBuffer.allocate(1) 168 | val expectedSecondUByte = UByte.MIN_VALUE 169 | second.writeUByte(expectedSecondUByte) 170 | second.resetForRead() 171 | 172 | val composableBuffer = FragmentedReadBuffer(first, second) 173 | assertEquals(expectedFirstUByte, composableBuffer.readUnsignedByte()) 174 | } 175 | 176 | @Test 177 | fun readUByteeFromSecondBuffer() { 178 | val expectedFirstUByte = UByte.MAX_VALUE 179 | val first = PlatformBuffer.allocate(1) 180 | first.writeUByte(expectedFirstUByte) 181 | first.resetForRead() 182 | val second = PlatformBuffer.allocate(1) 183 | val expectedSecondUByte = UByte.MIN_VALUE 184 | second.writeUByte(expectedSecondUByte) 185 | second.resetForRead() 186 | 187 | val composableBuffer = FragmentedReadBuffer(first, second) 188 | composableBuffer.position(1) 189 | assertEquals(expectedSecondUByte, composableBuffer.readUnsignedByte()) 190 | } 191 | 192 | @Test 193 | fun readUByteFromThreeBuffers() { 194 | val expectedFirstUByte = UByte.MAX_VALUE 195 | val first = PlatformBuffer.allocate(1) 196 | first.writeUByte(expectedFirstUByte) 197 | first.resetForRead() 198 | val second = PlatformBuffer.allocate(1) 199 | val expectedSecondUByte = 6.toUByte() 200 | second.writeUByte(expectedSecondUByte) 201 | second.resetForRead() 202 | val third = PlatformBuffer.allocate(1) 203 | val expectedThirdUByte = UByte.MIN_VALUE 204 | third.writeUByte(expectedThirdUByte) 205 | third.resetForRead() 206 | 207 | val composableBuffer = arrayListOf(first, second, third).toComposableBuffer() 208 | assertEquals(expectedFirstUByte, composableBuffer.readUnsignedByte()) 209 | assertEquals(expectedSecondUByte, composableBuffer.readUnsignedByte()) 210 | assertEquals(expectedThirdUByte, composableBuffer.readUnsignedByte()) 211 | } 212 | 213 | @Test 214 | fun readUByteFromFourBuffers() { 215 | val expectedFirstUByte = UByte.MAX_VALUE 216 | val first = PlatformBuffer.allocate(1) 217 | first.writeUByte(expectedFirstUByte) 218 | first.resetForRead() 219 | val second = PlatformBuffer.allocate(1) 220 | val expectedSecondUByte = 6.toUByte() 221 | second.writeUByte(expectedSecondUByte) 222 | second.resetForRead() 223 | val third = PlatformBuffer.allocate(1) 224 | val expectedThirdUByte = 12.toUByte() 225 | third.writeUByte(expectedThirdUByte) 226 | third.resetForRead() 227 | 228 | val fourth = PlatformBuffer.allocate(1) 229 | val expectedFourthUByte = UByte.MIN_VALUE 230 | fourth.writeUByte(expectedFourthUByte) 231 | fourth.resetForRead() 232 | 233 | val composableBuffer = arrayListOf(first, second, third, fourth).toComposableBuffer() 234 | assertEquals(expectedFirstUByte, composableBuffer.readUnsignedByte()) 235 | assertEquals(expectedSecondUByte, composableBuffer.readUnsignedByte()) 236 | assertEquals(expectedThirdUByte, composableBuffer.readUnsignedByte()) 237 | assertEquals(expectedFourthUByte, composableBuffer.readUnsignedByte()) 238 | } 239 | 240 | @Test 241 | fun readUByteFromFiveBuffers() { 242 | val expectedFirstUByte = UByte.MAX_VALUE 243 | val first = PlatformBuffer.allocate(1) 244 | first.writeUByte(expectedFirstUByte) 245 | first.resetForRead() 246 | val second = PlatformBuffer.allocate(1) 247 | val expectedSecondUByte = 6.toUByte() 248 | second.writeUByte(expectedSecondUByte) 249 | second.resetForRead() 250 | val third = PlatformBuffer.allocate(1) 251 | val expectedThirdUByte = (-1).toUByte() 252 | third.writeUByte(expectedThirdUByte) 253 | third.resetForRead() 254 | 255 | val fourth = PlatformBuffer.allocate(1) 256 | val expectedFourthUByte = 0.toUByte() 257 | fourth.writeUByte(expectedFourthUByte) 258 | fourth.resetForRead() 259 | 260 | val fifth = PlatformBuffer.allocate(1) 261 | val expectedFifthUByte = UByte.MIN_VALUE 262 | fifth.writeUByte(expectedFifthUByte) 263 | fifth.resetForRead() 264 | 265 | val composableBuffer = arrayListOf(first, second, third, fourth, fifth).toComposableBuffer() 266 | assertEquals(expectedFirstUByte, composableBuffer.readUnsignedByte()) 267 | assertEquals(expectedSecondUByte, composableBuffer.readUnsignedByte()) 268 | assertEquals(expectedThirdUByte, composableBuffer.readUnsignedByte()) 269 | assertEquals(expectedFourthUByte, composableBuffer.readUnsignedByte()) 270 | assertEquals(expectedFifthUByte, composableBuffer.readUnsignedByte()) 271 | } 272 | 273 | @Test 274 | fun readUnsignedShortFromFirstBuffer() { 275 | val expectedFirstUShort = UShort.MAX_VALUE 276 | val first = PlatformBuffer.allocate(UShort.SIZE_BYTES) 277 | first.writeUShort(expectedFirstUShort) 278 | first.resetForRead() 279 | 280 | val second = PlatformBuffer.allocate(UShort.SIZE_BYTES) 281 | val expectedSecondUShort = UShort.MIN_VALUE 282 | second.writeUShort(expectedSecondUShort) 283 | second.resetForRead() 284 | 285 | val composableBuffer = FragmentedReadBuffer(first, second) 286 | assertEquals(expectedFirstUShort, composableBuffer.readUnsignedShort()) 287 | assertEquals(expectedSecondUShort, composableBuffer.readUnsignedShort()) 288 | } 289 | 290 | @Test 291 | fun readUnsignedShortFromSecondBuffer() { 292 | val expectedFirstUShort = UShort.MAX_VALUE 293 | val first = PlatformBuffer.allocate(UShort.SIZE_BYTES) 294 | first.writeUShort(expectedFirstUShort) 295 | first.resetForRead() 296 | val second = PlatformBuffer.allocate(UShort.SIZE_BYTES) 297 | val expectedSecondUShort = UShort.MIN_VALUE 298 | second.writeUShort(expectedSecondUShort) 299 | second.resetForRead() 300 | 301 | val composableBuffer = FragmentedReadBuffer(first, second) 302 | composableBuffer.position(UShort.SIZE_BYTES) 303 | assertEquals(expectedSecondUShort, composableBuffer.readUnsignedShort()) 304 | } 305 | 306 | @Test 307 | fun readUnsignedShortsFromThreeBuffers() { 308 | val expectedFirstUShort = UShort.MAX_VALUE 309 | val first = PlatformBuffer.allocate(UShort.SIZE_BYTES) 310 | first.writeUShort(expectedFirstUShort) 311 | first.resetForRead() 312 | val second = PlatformBuffer.allocate(UShort.SIZE_BYTES) 313 | val expectedSecondUShort = 6.toUShort() 314 | second.writeUShort(expectedSecondUShort) 315 | second.resetForRead() 316 | val third = PlatformBuffer.allocate(UShort.SIZE_BYTES) 317 | val expectedThirdUShort = UShort.MIN_VALUE 318 | third.writeUShort(expectedThirdUShort) 319 | third.resetForRead() 320 | 321 | val composableBuffer = arrayListOf(first, second, third).toComposableBuffer() 322 | assertEquals(expectedFirstUShort, composableBuffer.readUnsignedShort()) 323 | assertEquals(expectedSecondUShort, composableBuffer.readUnsignedShort()) 324 | assertEquals(expectedThirdUShort, composableBuffer.readUnsignedShort()) 325 | } 326 | 327 | @Test 328 | fun readUnsignedShortsFromFourBuffers() { 329 | val expectedFirstUShort = UShort.MAX_VALUE 330 | val first = PlatformBuffer.allocate(UShort.SIZE_BYTES) 331 | first.writeUShort(expectedFirstUShort) 332 | first.resetForRead() 333 | val second = PlatformBuffer.allocate(UShort.SIZE_BYTES) 334 | val expectedSecondUShort = 6.toUShort() 335 | second.writeUShort(expectedSecondUShort) 336 | second.resetForRead() 337 | val third = PlatformBuffer.allocate(UShort.SIZE_BYTES) 338 | val expectedThirdUShort = 12.toUShort() 339 | third.writeUShort(expectedThirdUShort) 340 | third.resetForRead() 341 | 342 | val fourth = PlatformBuffer.allocate(UShort.SIZE_BYTES) 343 | val expectedFourthUShort = UShort.MIN_VALUE 344 | fourth.writeUShort(expectedFourthUShort) 345 | fourth.resetForRead() 346 | 347 | val composableBuffer = arrayListOf(first, second, third, fourth).toComposableBuffer() 348 | assertEquals(expectedFirstUShort, composableBuffer.readUnsignedShort()) 349 | assertEquals(expectedSecondUShort, composableBuffer.readUnsignedShort()) 350 | assertEquals(expectedThirdUShort, composableBuffer.readUnsignedShort()) 351 | assertEquals(expectedFourthUShort, composableBuffer.readUnsignedShort()) 352 | } 353 | 354 | @Test 355 | fun readUnsignedShortsFromFiveBuffers() { 356 | val expectedFirstUShort = UShort.MAX_VALUE 357 | val first = PlatformBuffer.allocate(UShort.SIZE_BYTES) 358 | first.writeUShort(expectedFirstUShort) 359 | first.resetForRead() 360 | val second = PlatformBuffer.allocate(UShort.SIZE_BYTES) 361 | val expectedSecondUShort = 6.toUShort() 362 | second.writeUShort(expectedSecondUShort) 363 | second.resetForRead() 364 | val third = PlatformBuffer.allocate(UShort.SIZE_BYTES) 365 | val expectedThirdUShort = (-1).toUShort() 366 | third.writeUShort(expectedThirdUShort) 367 | third.resetForRead() 368 | 369 | val fourth = PlatformBuffer.allocate(UShort.SIZE_BYTES) 370 | val expectedFourthUShort = 0.toUShort() 371 | fourth.writeUShort(expectedFourthUShort) 372 | fourth.resetForRead() 373 | 374 | val fifth = PlatformBuffer.allocate(UShort.SIZE_BYTES) 375 | val expectedFifthUShort = UShort.MIN_VALUE 376 | fifth.writeUShort(expectedFifthUShort) 377 | fifth.resetForRead() 378 | 379 | val composableBuffer = arrayListOf(first, second, third, fourth, fifth).toComposableBuffer() 380 | assertEquals(expectedFirstUShort, composableBuffer.readUnsignedShort()) 381 | assertEquals(expectedSecondUShort, composableBuffer.readUnsignedShort()) 382 | assertEquals(expectedThirdUShort, composableBuffer.readUnsignedShort()) 383 | assertEquals(expectedFourthUShort, composableBuffer.readUnsignedShort()) 384 | assertEquals(expectedFifthUShort, composableBuffer.readUnsignedShort()) 385 | } 386 | 387 | @Test 388 | fun readFragmentedStringFromThreeBuffers() { 389 | val expectedString = "yolo-swag-lyfestyle" 390 | val utf8length = expectedString.toReadBuffer(Charset.UTF8).limit() 391 | val composableBuffer = 392 | expectedString 393 | .split(Regex("(?=-)")) 394 | .map { it.toReadBuffer(Charset.UTF8) } 395 | .toComposableBuffer() 396 | val actual = composableBuffer.readString(utf8length, Charset.UTF8) 397 | assertEquals(expectedString, actual) 398 | } 399 | 400 | @Test 401 | fun utf8Line() { 402 | val buffers = arrayOf("yolo\r\n", "\nsw\n\r\nag", "\r\nli\n\r\nfe\r\nstyle\r\n") 403 | val composableBuffer = buffers.map { it.toReadBuffer(Charset.UTF8) }.toComposableBuffer() 404 | assertEquals("yolo", composableBuffer.readUtf8Line().toString()) 405 | assertEquals("", composableBuffer.readUtf8Line().toString()) 406 | assertEquals("sw", composableBuffer.readUtf8Line().toString()) 407 | assertEquals("", composableBuffer.readUtf8Line().toString()) 408 | assertEquals("ag", composableBuffer.readUtf8Line().toString()) 409 | assertEquals("li", composableBuffer.readUtf8Line().toString()) 410 | assertEquals("", composableBuffer.readUtf8Line().toString()) 411 | assertEquals("fe", composableBuffer.readUtf8Line().toString()) 412 | assertEquals("style", composableBuffer.readUtf8Line().toString()) 413 | assertEquals("", composableBuffer.readUtf8Line().toString()) 414 | assertTrue { composableBuffer.remaining() == 0 } 415 | } 416 | 417 | @Test 418 | fun largeFragmentedBuffer() { 419 | val buffers = mutableListOf() 420 | var indexCount = 0 421 | do { // 64 byte chunks 422 | val buffer = PlatformBuffer.allocate(64) 423 | repeat(64 / 4) { 424 | buffer.writeInt(indexCount++) 425 | } 426 | buffers += buffer 427 | } while (indexCount < 1024 * 1024) 428 | val fragmentedBuffer = buffers.toComposableBuffer() as FragmentedReadBuffer 429 | fragmentedBuffer.resetForRead() 430 | var intCount = 0 431 | fragmentedBuffer.walk { 432 | while (it.hasRemaining()) { 433 | assertEquals(intCount++, it.readInt()) 434 | } 435 | } 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/com/ditchoom/buffer/TransformedReadBufferTest.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class TransformedReadBufferTest { 7 | @Test 8 | fun byte() { 9 | val buffer = PlatformBuffer.allocate(Byte.SIZE_BYTES) 10 | buffer.writeByte(10.toByte()) 11 | buffer.resetForRead() 12 | val add1TransformedReadBuffer = 13 | TransformedReadBuffer(buffer) { _, byte -> 14 | (byte + 1).toByte() 15 | } 16 | assertEquals(11.toByte(), add1TransformedReadBuffer.readByte()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/com/ditchoom/buffer/BufferFactory.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import js.buffer.SharedArrayBuffer 4 | import org.khronos.webgl.ArrayBuffer 5 | import org.khronos.webgl.Int8Array 6 | 7 | fun PlatformBuffer.Companion.allocate( 8 | size: Int, 9 | byteOrder: ByteOrder, 10 | ) = allocate(size, AllocationZone.SharedMemory, byteOrder) 11 | 12 | actual fun PlatformBuffer.Companion.allocate( 13 | size: Int, 14 | zone: AllocationZone, 15 | byteOrder: ByteOrder, 16 | ): PlatformBuffer { 17 | if (zone is AllocationZone.Custom) { 18 | return zone.allocator(size) 19 | } 20 | val sharedArrayBuffer = 21 | try { 22 | if (zone is AllocationZone.SharedMemory) { 23 | SharedArrayBuffer(size) 24 | } else { 25 | null 26 | } 27 | } catch (t: Throwable) { 28 | null 29 | } 30 | if (sharedArrayBuffer == null && zone is AllocationZone.SharedMemory) { 31 | console.warn( 32 | "Failed to allocate shared buffer in BufferFactory.kt. Please check and validate the " + 33 | "appropriate headers are set on the http request as defined in the SharedArrayBuffer MDN docs." + 34 | "see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects" + 35 | "/SharedArrayBuffer#security_requirements", 36 | ) 37 | } 38 | return if (sharedArrayBuffer != null) { 39 | val arrayBuffer = sharedArrayBuffer.unsafeCast().slice(0, size) 40 | JsBuffer( 41 | Int8Array(arrayBuffer), 42 | littleEndian = byteOrder == ByteOrder.LITTLE_ENDIAN, 43 | sharedArrayBuffer = sharedArrayBuffer, 44 | ) 45 | } else { 46 | JsBuffer(Int8Array(size), littleEndian = byteOrder == ByteOrder.LITTLE_ENDIAN) 47 | } 48 | } 49 | 50 | actual fun PlatformBuffer.Companion.wrap( 51 | array: ByteArray, 52 | byteOrder: ByteOrder, 53 | ): PlatformBuffer = 54 | JsBuffer( 55 | array.unsafeCast(), 56 | littleEndian = byteOrder == ByteOrder.LITTLE_ENDIAN, 57 | ) 58 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/com/ditchoom/buffer/JsBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import js.buffer.BufferSource 4 | import js.buffer.SharedArrayBuffer 5 | import org.khronos.webgl.DataView 6 | import org.khronos.webgl.Int8Array 7 | import web.encoding.TextDecoder 8 | import web.encoding.TextDecoderOptions 9 | 10 | data class JsBuffer( 11 | val buffer: Int8Array, 12 | // network endian is big endian 13 | private val littleEndian: Boolean = false, 14 | private var position: Int = 0, 15 | private var limit: Int = 0, 16 | override val capacity: Int = buffer.byteLength, 17 | val sharedArrayBuffer: SharedArrayBuffer? = null, 18 | ) : PlatformBuffer { 19 | override val byteOrder = if (littleEndian) ByteOrder.LITTLE_ENDIAN else ByteOrder.BIG_ENDIAN 20 | 21 | init { 22 | limit = buffer.length 23 | } 24 | 25 | override fun resetForRead() { 26 | limit = position 27 | position = 0 28 | } 29 | 30 | override fun resetForWrite() { 31 | position = 0 32 | limit = capacity 33 | } 34 | 35 | override fun setLimit(limit: Int) { 36 | this.limit = limit 37 | } 38 | 39 | fun setPosition(position: Int) { 40 | this.position = position 41 | } 42 | 43 | override fun readByte(): Byte { 44 | val dataView = DataView(buffer.buffer, position++, 1) 45 | return dataView.getInt8(0) 46 | } 47 | 48 | override fun get(index: Int): Byte { 49 | val dataView = DataView(buffer.buffer, index, 1) 50 | return dataView.getInt8(0) 51 | } 52 | 53 | override fun slice(): ReadBuffer { 54 | return JsBuffer( 55 | Int8Array(buffer.buffer.slice(position, limit)), 56 | littleEndian, 57 | sharedArrayBuffer = sharedArrayBuffer, 58 | ) 59 | } 60 | 61 | override fun readByteArray(size: Int): ByteArray { 62 | val subArray = buffer.subarray(position, position + size) 63 | val byteArray = Int8Array(subArray.buffer, subArray.byteOffset, size) 64 | position += size 65 | return byteArray.unsafeCast() 66 | } 67 | 68 | override fun readShort(): Short { 69 | val dataView = DataView(buffer.buffer, position, Short.SIZE_BYTES) 70 | position += Short.SIZE_BYTES 71 | return dataView.getInt16(0, littleEndian).toInt().toShort() 72 | } 73 | 74 | override fun getShort(index: Int): Short { 75 | val dataView = DataView(buffer.buffer, index, Short.SIZE_BYTES) 76 | return dataView.getInt16(0, littleEndian).toInt().toShort() 77 | } 78 | 79 | override fun readInt(): Int { 80 | val dataView = DataView(buffer.buffer, position, Int.SIZE_BYTES) 81 | position += Int.SIZE_BYTES 82 | return dataView.getInt32(0, littleEndian) 83 | } 84 | 85 | override fun getInt(index: Int): Int { 86 | val dataView = DataView(buffer.buffer, index, Int.SIZE_BYTES) 87 | return dataView.getInt32(0, littleEndian) 88 | } 89 | 90 | override fun readLong(): Long { 91 | val bytes = readByteArray(Long.SIZE_BYTES) 92 | return if (littleEndian) bytes.reversedArray().toLong() else bytes.toLong() 93 | } 94 | 95 | override fun getLong(index: Int): Long { 96 | val bytes = Int8Array(buffer.buffer, index, Long.SIZE_BYTES).unsafeCast() 97 | return if (littleEndian) bytes.reversedArray().toLong() else bytes.toLong() 98 | } 99 | 100 | private fun ByteArray.toLong(): Long { 101 | var result: Long = 0 102 | (0 until Long.SIZE_BYTES).forEach { 103 | result = result shl Long.SIZE_BYTES 104 | result = result or (this[it].toLong() and 0xFFL) 105 | } 106 | return result 107 | } 108 | 109 | override fun readString( 110 | length: Int, 111 | charset: Charset, 112 | ): String { 113 | val encoding = 114 | when (charset) { 115 | Charset.UTF8 -> "utf-8" 116 | Charset.UTF16 -> throw UnsupportedOperationException("Not sure how to implement") 117 | Charset.UTF16BigEndian -> "utf-16be" 118 | Charset.UTF16LittleEndian -> "utf-16le" 119 | Charset.ASCII -> "ascii" 120 | Charset.ISOLatin1 -> "iso-8859-1" 121 | Charset.UTF32 -> throw UnsupportedOperationException("Not sure how to implement") 122 | Charset.UTF32LittleEndian -> throw UnsupportedOperationException("Not sure how to implement") 123 | Charset.UTF32BigEndian -> throw UnsupportedOperationException("Not sure how to implement") 124 | } 125 | 126 | @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") 127 | val textDecoder = TextDecoder(encoding, js("{fatal: true}") as TextDecoderOptions) 128 | val result = 129 | textDecoder.decode( 130 | buffer.subarray(position, position + length).unsafeCast(), 131 | ) 132 | position(position + length) 133 | return result 134 | } 135 | 136 | override fun write(buffer: ReadBuffer) { 137 | val size = buffer.remaining() 138 | if (buffer is JsBuffer) { 139 | this.buffer.set(buffer.buffer, position) 140 | } else { 141 | this.buffer.set(buffer.readByteArray(size).toTypedArray(), position) 142 | } 143 | position += size 144 | buffer.position(buffer.position() + size) 145 | } 146 | 147 | override fun writeByte(byte: Byte): WriteBuffer { 148 | val dataView = DataView(buffer.buffer, position++, Byte.SIZE_BYTES) 149 | dataView.setInt8(0, byte) 150 | return this 151 | } 152 | 153 | override fun set( 154 | index: Int, 155 | byte: Byte, 156 | ): WriteBuffer { 157 | val dataView = DataView(buffer.buffer, index, Byte.SIZE_BYTES) 158 | dataView.setInt8(0, byte) 159 | return this 160 | } 161 | 162 | override fun writeBytes( 163 | bytes: ByteArray, 164 | offset: Int, 165 | length: Int, 166 | ): WriteBuffer { 167 | val int8Array = bytes.unsafeCast().subarray(offset, offset + length) 168 | this.buffer.set(int8Array, position) 169 | position += int8Array.length 170 | return this 171 | } 172 | 173 | override fun writeShort(short: Short): WriteBuffer { 174 | val dataView = DataView(buffer.buffer, position, Short.SIZE_BYTES) 175 | position += UShort.SIZE_BYTES 176 | dataView.setUint16(0, short, littleEndian) 177 | return this 178 | } 179 | 180 | override fun set( 181 | index: Int, 182 | short: Short, 183 | ): WriteBuffer { 184 | val dataView = DataView(buffer.buffer, index, Short.SIZE_BYTES) 185 | dataView.setUint16(0, short, littleEndian) 186 | return this 187 | } 188 | 189 | override fun writeInt(int: Int): WriteBuffer { 190 | val dataView = DataView(buffer.buffer, position, UInt.SIZE_BYTES) 191 | position += UInt.SIZE_BYTES 192 | dataView.setInt32(0, int, littleEndian) 193 | return this 194 | } 195 | 196 | override fun set( 197 | index: Int, 198 | int: Int, 199 | ): WriteBuffer { 200 | val dataView = DataView(buffer.buffer, index, UInt.SIZE_BYTES) 201 | dataView.setUint32(0, int, littleEndian) 202 | return this 203 | } 204 | 205 | override fun writeLong(long: Long): WriteBuffer { 206 | val bytes = if (littleEndian) long.toByteArray().reversedArray() else long.toByteArray() 207 | writeBytes(bytes) 208 | return this 209 | } 210 | 211 | override fun set( 212 | index: Int, 213 | long: Long, 214 | ): WriteBuffer { 215 | val bytes = if (littleEndian) long.toByteArray().reversedArray() else long.toByteArray() 216 | val int8Array = bytes.unsafeCast().subarray(0, Long.SIZE_BYTES) 217 | this.buffer.set(int8Array, index) 218 | return this 219 | } 220 | 221 | private fun Long.toByteArray(): ByteArray { 222 | var l = this 223 | val result = ByteArray(8) 224 | for (i in 7 downTo 0) { 225 | result[i] = l.toByte() 226 | l = l shr 8 227 | } 228 | return result 229 | } 230 | 231 | override fun writeString( 232 | text: CharSequence, 233 | charset: Charset, 234 | ): WriteBuffer { 235 | when (charset) { 236 | Charset.UTF8 -> writeBytes(text.toString().encodeToByteArray()) 237 | else -> throw UnsupportedOperationException("Unable to encode in $charset. Must use Charset.UTF8") 238 | } 239 | return this 240 | } 241 | 242 | override fun limit() = limit 243 | 244 | override fun position() = position 245 | 246 | override fun position(newPosition: Int) { 247 | position = newPosition 248 | } 249 | 250 | override suspend fun close() {} 251 | 252 | override fun equals(other: Any?): Boolean { 253 | if (this === other) return true 254 | if (other == null || this::class.js != other::class.js) return false 255 | 256 | other as JsBuffer 257 | 258 | if (littleEndian != other.littleEndian) return false 259 | if (position != other.position) return false 260 | if (limit != other.limit) return false 261 | if (capacity != other.capacity) return false 262 | val size = remaining() 263 | try { 264 | if (!readByteArray(size).contentEquals(other.readByteArray(size))) return false 265 | } finally { 266 | position -= size 267 | other.position -= size 268 | } 269 | return true 270 | } 271 | 272 | override fun hashCode(): Int { 273 | var result = littleEndian.hashCode() 274 | result = 31 * result + position 275 | result = 31 * result + limit 276 | result = 31 * result + capacity.hashCode() 277 | val size = remaining() 278 | try { 279 | result = 31 * result + readByteArray(size).hashCode() 280 | } finally { 281 | position -= size 282 | } 283 | return result 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/com/ditchoom/buffer/Parcelable.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 4 | actual interface Parcelable 5 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/ditchoom/buffer/BaseJvmBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import java.io.RandomAccessFile 4 | import java.nio.Buffer 5 | import java.nio.ByteBuffer 6 | import java.nio.CharBuffer 7 | import java.nio.charset.CodingErrorAction 8 | import kotlin.coroutines.resume 9 | import kotlin.coroutines.resumeWithException 10 | import kotlin.coroutines.suspendCoroutine 11 | 12 | abstract class BaseJvmBuffer(val byteBuffer: ByteBuffer, val fileRef: RandomAccessFile? = null) : 13 | PlatformBuffer { 14 | override val byteOrder = 15 | when (byteBuffer.order()) { 16 | java.nio.ByteOrder.BIG_ENDIAN -> ByteOrder.BIG_ENDIAN 17 | java.nio.ByteOrder.LITTLE_ENDIAN -> ByteOrder.LITTLE_ENDIAN 18 | else -> ByteOrder.BIG_ENDIAN 19 | } 20 | 21 | // Use Buffer reference to avoid NoSuchMethodException between JVM. see https://stackoverflow.com/q/61267495 22 | private val buffer = byteBuffer as Buffer 23 | 24 | override fun resetForRead() { 25 | buffer.flip() 26 | } 27 | 28 | override fun resetForWrite() { 29 | buffer.clear() 30 | } 31 | 32 | override fun setLimit(limit: Int) { 33 | buffer.limit(limit) 34 | } 35 | 36 | override val capacity = buffer.capacity() 37 | 38 | override fun readByte() = byteBuffer.get() 39 | 40 | override fun get(index: Int): Byte = byteBuffer.get(index) 41 | 42 | override fun readByteArray(size: Int) = byteBuffer.toArray(size) 43 | 44 | override fun slice() = JvmBuffer(byteBuffer.slice()) 45 | 46 | override fun readShort(): Short = byteBuffer.short 47 | 48 | override fun getShort(index: Int): Short = byteBuffer.getShort(index) 49 | 50 | override fun readInt() = byteBuffer.int 51 | 52 | override fun getInt(index: Int): Int = byteBuffer.getInt(index) 53 | 54 | override fun readLong() = byteBuffer.long 55 | 56 | override fun getLong(index: Int): Long = byteBuffer.getLong(index) 57 | 58 | override fun readString( 59 | length: Int, 60 | charset: Charset, 61 | ): String { 62 | val finalPosition = buffer.position() + length 63 | val readBuffer = byteBuffer.asReadOnlyBuffer() 64 | (readBuffer as Buffer).limit(finalPosition) 65 | val charsetConverted = 66 | when (charset) { 67 | Charset.UTF8 -> Charsets.UTF_8 68 | Charset.UTF16 -> Charsets.UTF_16 69 | Charset.UTF16BigEndian -> Charsets.UTF_16BE 70 | Charset.UTF16LittleEndian -> Charsets.UTF_16LE 71 | Charset.ASCII -> Charsets.US_ASCII 72 | Charset.ISOLatin1 -> Charsets.ISO_8859_1 73 | Charset.UTF32 -> Charsets.UTF_32 74 | Charset.UTF32LittleEndian -> Charsets.UTF_32LE 75 | Charset.UTF32BigEndian -> Charsets.UTF_32BE 76 | } 77 | val decoded = 78 | charsetConverted.newDecoder() 79 | .onMalformedInput(CodingErrorAction.REPORT) 80 | .onUnmappableCharacter(CodingErrorAction.REPORT) 81 | .decode(readBuffer) 82 | buffer.position(finalPosition) 83 | return decoded.toString() 84 | } 85 | 86 | override fun writeByte(byte: Byte): WriteBuffer { 87 | byteBuffer.put(byte) 88 | return this 89 | } 90 | 91 | override fun set( 92 | index: Int, 93 | byte: Byte, 94 | ): WriteBuffer { 95 | byteBuffer.put(index, byte) 96 | return this 97 | } 98 | 99 | override fun writeBytes( 100 | bytes: ByteArray, 101 | offset: Int, 102 | length: Int, 103 | ): WriteBuffer { 104 | byteBuffer.put(bytes, offset, length) 105 | return this 106 | } 107 | 108 | override fun writeShort(short: Short): WriteBuffer { 109 | byteBuffer.putShort(short) 110 | return this 111 | } 112 | 113 | override fun set( 114 | index: Int, 115 | short: Short, 116 | ): WriteBuffer { 117 | byteBuffer.putShort(index, short) 118 | return this 119 | } 120 | 121 | override fun writeInt(int: Int): WriteBuffer { 122 | byteBuffer.putInt(int) 123 | return this 124 | } 125 | 126 | override fun set( 127 | index: Int, 128 | int: Int, 129 | ): WriteBuffer { 130 | byteBuffer.putInt(index, int) 131 | return this 132 | } 133 | 134 | override fun writeLong(long: Long): WriteBuffer { 135 | byteBuffer.putLong(long) 136 | return this 137 | } 138 | 139 | override fun set( 140 | index: Int, 141 | long: Long, 142 | ): WriteBuffer { 143 | byteBuffer.putLong(index, long) 144 | return this 145 | } 146 | 147 | override fun writeString( 148 | text: CharSequence, 149 | charset: Charset, 150 | ): WriteBuffer { 151 | val encoder = charset.toEncoder() 152 | encoder.reset() 153 | encoder.encode(CharBuffer.wrap(text), byteBuffer, true) 154 | return this 155 | } 156 | 157 | override fun write(buffer: ReadBuffer) { 158 | if (buffer is JvmBuffer) { 159 | byteBuffer.put(buffer.byteBuffer) 160 | } else { 161 | byteBuffer.put(buffer.readByteArray(buffer.remaining())) 162 | } 163 | } 164 | 165 | override fun position(newPosition: Int) { 166 | buffer.position(newPosition) 167 | } 168 | 169 | override fun equals(other: Any?): Boolean { 170 | if (other !is PlatformBuffer) return false 171 | if (position() != other.position()) return false 172 | if (limit() != other.limit()) return false 173 | if (capacity != other.capacity) return false 174 | return true 175 | } 176 | 177 | override fun toString() = "Buffer[pos=${position()} lim=${limit()} cap=$capacity]" 178 | 179 | override suspend fun close() { 180 | fileRef?.aClose() 181 | } 182 | 183 | override fun limit() = buffer.limit() 184 | 185 | override fun position() = buffer.position() 186 | } 187 | 188 | suspend fun RandomAccessFile.aClose() = 189 | suspendCoroutine { 190 | try { 191 | // TODO: fix the blocking call 192 | @Suppress("BlockingMethodInNonBlockingContext") 193 | close() 194 | it.resume(Unit) 195 | } catch (e: Throwable) { 196 | it.resumeWithException(e) 197 | } 198 | } 199 | 200 | fun ByteBuffer.toArray(size: Int = remaining()): ByteArray { 201 | return if (hasArray()) { 202 | val result = ByteArray(size) 203 | val buffer = this as Buffer 204 | System.arraycopy(this.array(), buffer.arrayOffset() + buffer.position(), result, 0, size) 205 | buffer.position(buffer.position() + size) 206 | result 207 | } else { 208 | val byteArray = ByteArray(size) 209 | get(byteArray) 210 | byteArray 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/ditchoom/buffer/BufferFactory.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("BufferFactoryJvm") 2 | 3 | package com.ditchoom.buffer 4 | 5 | import java.nio.ByteBuffer 6 | 7 | actual fun PlatformBuffer.Companion.allocate( 8 | size: Int, 9 | zone: AllocationZone, 10 | byteOrder: ByteOrder, 11 | ): PlatformBuffer { 12 | val byteOrderNative = 13 | when (byteOrder) { 14 | ByteOrder.BIG_ENDIAN -> java.nio.ByteOrder.BIG_ENDIAN 15 | ByteOrder.LITTLE_ENDIAN -> java.nio.ByteOrder.LITTLE_ENDIAN 16 | } 17 | return when (zone) { 18 | AllocationZone.Heap -> JvmBuffer(ByteBuffer.allocate(size).order(byteOrderNative)) 19 | AllocationZone.SharedMemory, 20 | AllocationZone.Direct, 21 | -> JvmBuffer(ByteBuffer.allocateDirect(size).order(byteOrderNative)) 22 | 23 | is AllocationZone.Custom -> zone.allocator(size) 24 | } 25 | } 26 | 27 | actual fun PlatformBuffer.Companion.wrap( 28 | array: ByteArray, 29 | byteOrder: ByteOrder, 30 | ): PlatformBuffer { 31 | val byteOrderNative = 32 | when (byteOrder) { 33 | ByteOrder.BIG_ENDIAN -> java.nio.ByteOrder.BIG_ENDIAN 34 | ByteOrder.LITTLE_ENDIAN -> java.nio.ByteOrder.LITTLE_ENDIAN 35 | } 36 | return JvmBuffer(ByteBuffer.wrap(array).order(byteOrderNative)) 37 | } 38 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/ditchoom/buffer/CharsetEncoderHelper.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import java.nio.charset.CharsetEncoder 4 | 5 | fun Charset.toEncoder(): CharsetEncoder = 6 | when (this) { 7 | Charset.UTF8 -> utf8Encoder 8 | Charset.UTF16 -> utf16Encoder 9 | Charset.UTF16BigEndian -> utf16BEEncoder 10 | Charset.UTF16LittleEndian -> utf16LEEncoder 11 | Charset.ASCII -> utfAsciiEncoder 12 | Charset.ISOLatin1 -> utfISOLatin1Encoder 13 | Charset.UTF32 -> utf32Encoder 14 | Charset.UTF32LittleEndian -> utf32LEEncoder 15 | Charset.UTF32BigEndian -> utf32BEEncoder 16 | }.get() 17 | 18 | internal class DefaultEncoder(private val charset: java.nio.charset.Charset) : 19 | ThreadLocal() { 20 | override fun initialValue(): CharsetEncoder? = charset.newEncoder() 21 | 22 | override fun get(): CharsetEncoder = super.get()!! 23 | } 24 | 25 | internal val utf8Encoder = DefaultEncoder(Charsets.UTF_8) 26 | internal val utf16Encoder = DefaultEncoder(Charsets.UTF_16) 27 | internal val utf16BEEncoder = DefaultEncoder(Charsets.UTF_16BE) 28 | internal val utf16LEEncoder = DefaultEncoder(Charsets.UTF_16LE) 29 | internal val utfAsciiEncoder = DefaultEncoder(Charsets.US_ASCII) 30 | internal val utfISOLatin1Encoder = DefaultEncoder(Charsets.ISO_8859_1) 31 | internal val utf32Encoder = DefaultEncoder(Charsets.UTF_32) 32 | internal val utf32LEEncoder = DefaultEncoder(Charsets.UTF_32LE) 33 | internal val utf32BEEncoder = DefaultEncoder(Charsets.UTF_32BE) 34 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/ditchoom/buffer/JvmBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | import java.io.RandomAccessFile 4 | import java.nio.ByteBuffer 5 | 6 | class JvmBuffer(byteBuffer: ByteBuffer, fileRef: RandomAccessFile? = null) : 7 | BaseJvmBuffer(byteBuffer, fileRef) 8 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/com/ditchoom/buffer/Parcelable.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 4 | actual interface Parcelable 5 | -------------------------------------------------------------------------------- /src/linuxMain/kotlin/com/ditchoom/buffer/BufferFactory.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | actual fun PlatformBuffer.Companion.allocate( 4 | size: Int, 5 | zone: AllocationZone, 6 | byteOrder: ByteOrder, 7 | ): PlatformBuffer { 8 | if (zone is AllocationZone.Custom) { 9 | return zone.allocator(size) 10 | } 11 | return NativeBuffer(ByteArray(size), byteOrder = byteOrder) 12 | } 13 | 14 | actual fun PlatformBuffer.Companion.wrap( 15 | array: ByteArray, 16 | byteOrder: ByteOrder, 17 | ): PlatformBuffer = NativeBuffer(array, byteOrder = byteOrder) 18 | -------------------------------------------------------------------------------- /src/nativeMain/kotlin/buffer/NativeBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | data class NativeBuffer( 4 | val data: ByteArray, 5 | private var position: Int = 0, 6 | private var limit: Int = data.size, 7 | override val capacity: Int = data.size, 8 | override val byteOrder: ByteOrder, 9 | ) : PlatformBuffer { 10 | override fun resetForRead() { 11 | limit = position 12 | position = 0 13 | } 14 | 15 | override fun resetForWrite() { 16 | position = 0 17 | limit = data.size 18 | } 19 | 20 | override fun setLimit(limit: Int) { 21 | this.limit = limit 22 | } 23 | 24 | override fun readByte() = data[position++] 25 | 26 | override fun get(index: Int): Byte = data[index] 27 | 28 | override fun slice(): ReadBuffer { 29 | return NativeBuffer(data.sliceArray(position until limit), byteOrder = byteOrder) 30 | } 31 | 32 | override fun readByteArray(size: Int): ByteArray { 33 | val result = data.copyOfRange(position, position + size) 34 | position += size 35 | return result 36 | } 37 | 38 | override fun readString( 39 | length: Int, 40 | charset: Charset, 41 | ): String { 42 | val value = 43 | when (charset) { 44 | Charset.UTF8 -> data.decodeToString(position, position + length, throwOnInvalidSequence = true) 45 | Charset.UTF16 -> throw UnsupportedOperationException("Not sure how to implement.") 46 | Charset.UTF16BigEndian -> throw UnsupportedOperationException("Not sure how to implement.") 47 | Charset.UTF16LittleEndian -> throw UnsupportedOperationException("Not sure how to implement.") 48 | Charset.ASCII -> throw UnsupportedOperationException("Not sure how to implement.") 49 | Charset.ISOLatin1 -> throw UnsupportedOperationException("Not sure how to implement.") 50 | Charset.UTF32 -> throw UnsupportedOperationException("Not sure how to implement.") 51 | Charset.UTF32LittleEndian -> throw UnsupportedOperationException("Not sure how to implement.") 52 | Charset.UTF32BigEndian -> throw UnsupportedOperationException("Not sure how to implement.") 53 | } 54 | position += length 55 | return value 56 | } 57 | 58 | override fun writeByte(byte: Byte): WriteBuffer { 59 | data[position++] = byte 60 | return this 61 | } 62 | 63 | override fun set( 64 | index: Int, 65 | byte: Byte, 66 | ): WriteBuffer { 67 | data[index] = byte 68 | return this 69 | } 70 | 71 | override fun writeBytes( 72 | bytes: ByteArray, 73 | offset: Int, 74 | length: Int, 75 | ): WriteBuffer { 76 | bytes.copyInto(data, position, offset, offset + length) 77 | position += length 78 | return this 79 | } 80 | 81 | override fun write(buffer: ReadBuffer) { 82 | val start = position() 83 | val byteSize = 84 | if (buffer is NativeBuffer) { 85 | writeBytes(buffer.data) 86 | buffer.data.size 87 | } else { 88 | val numBytes = buffer.remaining() 89 | writeBytes(buffer.readByteArray(numBytes)) 90 | numBytes 91 | } 92 | buffer.position(start + byteSize) 93 | } 94 | 95 | override fun writeString( 96 | text: CharSequence, 97 | charset: Charset, 98 | ): WriteBuffer { 99 | when (charset) { 100 | Charset.UTF8 -> writeBytes(text.toString().encodeToByteArray()) 101 | else -> throw UnsupportedOperationException("Unable to encode in $charset. Must use Charset.UTF8") 102 | } 103 | return this 104 | } 105 | 106 | override suspend fun close() = Unit 107 | 108 | override fun limit() = limit 109 | 110 | override fun position() = position 111 | 112 | override fun position(newPosition: Int) { 113 | position = newPosition 114 | } 115 | 116 | override fun equals(other: Any?): Boolean { 117 | if (this === other) return true 118 | other as NativeBuffer 119 | if (position != other.position) return false 120 | if (limit != other.limit) return false 121 | if (capacity != other.capacity) return false 122 | if (!data.contentEquals(other.data)) return false 123 | return true 124 | } 125 | 126 | override fun hashCode(): Int { 127 | var result = position.hashCode() 128 | result = 31 * result + limit.hashCode() 129 | result = 31 * result + capacity.hashCode() 130 | result = 31 * result + data.contentHashCode() 131 | return result 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/nativeMain/kotlin/buffer/Parcelable.kt: -------------------------------------------------------------------------------- 1 | package com.ditchoom.buffer 2 | 3 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 4 | actual interface Parcelable 5 | -------------------------------------------------------------------------------- /webpack.config.d/config.js: -------------------------------------------------------------------------------- 1 | if (config.devServer != null) { 2 | config.devServer.headers = { 3 | "Cross-Origin-Opener-Policy": "same-origin", 4 | "Cross-Origin-Embedder-Policy": "require-corp" 5 | } 6 | } --------------------------------------------------------------------------------