aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorarangelov <arangelov@google.com>2021-04-19 14:40:09 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-04-19 14:40:09 +0000
commit518e892b1f4228a8d79a6f65138fcb29922ea383 (patch)
treef23349a7ab3321ab830d3a9021bf74abdcc2fd84
parent9591bd1f133e194a299f0eb8b2d14cd809a4e95c (diff)
parentdb42b8a834f8a0372dc4f2eaab6dc638a7ca1023 (diff)
downloadokio-518e892b1f4228a8d79a6f65138fcb29922ea383.tar.gz
Add okio library am: 3f7da6ef12 am: db42b8a834
Original change: https://android-review.googlesource.com/c/platform/external/okio/+/1679485 Change-Id: I3a3d08a2db9e93e9ee95fc16378cee50eee6d62a
-rwxr-xr-x.buildscript/prepare_mkdocs.sh16
-rw-r--r--.buildscript/restore_v1_docs.sh30
-rw-r--r--.editorconfig5
-rw-r--r--.github/workflows/build.yml162
-rw-r--r--.gitignore30
-rw-r--r--Android.bp15
-rw-r--r--BUG-BOUNTY.md10
-rw-r--r--CHANGELOG.md583
-rw-r--r--CONTRIBUTING.md38
l---------LICENSE1
-rw-r--r--LICENSE.txt202
-rw-r--r--METADATA20
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS3
-rw-r--r--README.md29
-rw-r--r--android-test/README.md50
-rw-r--r--android-test/build.gradle73
-rw-r--r--android-test/multidex-config.pro1
-rw-r--r--android-test/src/main/AndroidManifest.xml16
-rw-r--r--android-test/src/main/res/values/strings.xml3
-rw-r--r--android-test/src/main/res/xml/network_security_config.xml5
-rw-r--r--build.gradle120
-rw-r--r--docs/code_of_conduct.md102
-rw-r--r--docs/css/app.css48
-rw-r--r--docs/css/dokka-logo.css6
-rw-r--r--docs/images/icon-square.pngbin0 -> 1469 bytes
-rw-r--r--docs/images/logo-square.pngbin0 -> 3740 bytes
-rw-r--r--docs/index.md1163
-rw-r--r--docs/multiplatform.md40
-rw-r--r--docs/releasing.md105
-rw-r--r--docs/security.md19
-rw-r--r--gradle.properties25
-rw-r--r--gradle/gradle-mvn-mpp-push.gradle137
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 59203 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xgradlew185
-rw-r--r--gradlew.bat89
-rw-r--r--mkdocs.yml59
-rw-r--r--okio/build.gradle193
-rw-r--r--okio/gradle.properties2
-rw-r--r--okio/jvm/japicmp/build.gradle56
-rw-r--r--okio/jvm/jmh/README.md10
-rw-r--r--okio/jvm/jmh/build.gradle36
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BenchmarkUtils.kt32
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferCursorSeekBenchmark.java105
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferPerformanceBenchmark.java330
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferUtf8Benchmark.java134
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/GetByteBenchmark.java75
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/HashFunctionBenchmark.java66
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/IndexOfElementBenchmark.java77
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/ReadByteStringBenchmark.java72
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/SegmentedByteStringBenchmark.java103
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/SelectBenchmark.java102
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/Utf8Benchmark.java142
-rw-r--r--okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/WriteHexadecimalBenchmark.java70
-rw-r--r--okio/jvm/jvm.gradle26
-rw-r--r--okio/src/appleMain/kotlin/okio/ByteString.kt32
-rw-r--r--okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt31
-rw-r--r--okio/src/commonMain/kotlin/okio/-Base64.kt148
-rw-r--r--okio/src/commonMain/kotlin/okio/-Platform.kt43
-rw-r--r--okio/src/commonMain/kotlin/okio/-Util.kt163
-rw-r--r--okio/src/commonMain/kotlin/okio/Buffer.kt408
-rw-r--r--okio/src/commonMain/kotlin/okio/BufferedSink.kt314
-rw-r--r--okio/src/commonMain/kotlin/okio/BufferedSource.kt534
-rw-r--r--okio/src/commonMain/kotlin/okio/ByteString.kt197
-rw-r--r--okio/src/commonMain/kotlin/okio/ExperimentalFileSystem.kt27
-rw-r--r--okio/src/commonMain/kotlin/okio/HashingSink.kt66
-rw-r--r--okio/src/commonMain/kotlin/okio/HashingSource.kt67
-rw-r--r--okio/src/commonMain/kotlin/okio/Okio.kt70
-rw-r--r--okio/src/commonMain/kotlin/okio/Options.kt235
-rw-r--r--okio/src/commonMain/kotlin/okio/PeekSource.kt73
-rw-r--r--okio/src/commonMain/kotlin/okio/RealBufferedSink.kt24
-rw-r--r--okio/src/commonMain/kotlin/okio/RealBufferedSource.kt24
-rw-r--r--okio/src/commonMain/kotlin/okio/Segment.kt184
-rw-r--r--okio/src/commonMain/kotlin/okio/SegmentPool.kt36
-rw-r--r--okio/src/commonMain/kotlin/okio/SegmentedByteString.kt50
-rw-r--r--okio/src/commonMain/kotlin/okio/Sink.kt63
-rw-r--r--okio/src/commonMain/kotlin/okio/Source.kt70
-rw-r--r--okio/src/commonMain/kotlin/okio/Timeout.kt44
-rw-r--r--okio/src/commonMain/kotlin/okio/Utf8.kt557
-rw-r--r--okio/src/commonMain/kotlin/okio/internal/-Utf8.kt59
-rw-r--r--okio/src/commonMain/kotlin/okio/internal/Buffer.kt1688
-rw-r--r--okio/src/commonMain/kotlin/okio/internal/ByteString.kt344
-rw-r--r--okio/src/commonMain/kotlin/okio/internal/RealBufferedSink.kt215
-rw-r--r--okio/src/commonMain/kotlin/okio/internal/RealBufferedSource.kt398
-rw-r--r--okio/src/commonMain/kotlin/okio/internal/SegmentedByteString.kt226
-rw-r--r--okio/src/commonTest/kotlin/okio/AbstractBufferedSinkTest.kt261
-rw-r--r--okio/src/commonTest/kotlin/okio/AbstractBufferedSourceTest.kt1281
-rw-r--r--okio/src/commonTest/kotlin/okio/BufferCommonTest.kt90
-rw-r--r--okio/src/commonTest/kotlin/okio/BufferedSinkFactory.kt36
-rw-r--r--okio/src/commonTest/kotlin/okio/BufferedSourceFactory.kt150
-rw-r--r--okio/src/commonTest/kotlin/okio/ByteStringFactory.kt53
-rw-r--r--okio/src/commonTest/kotlin/okio/ByteStringTest.kt499
-rw-r--r--okio/src/commonTest/kotlin/okio/CommonBufferTest.kt430
-rw-r--r--okio/src/commonTest/kotlin/okio/CommonOkioKotlinTest.kt42
-rw-r--r--okio/src/commonTest/kotlin/okio/CommonOptionsTest.kt440
-rw-r--r--okio/src/commonTest/kotlin/okio/CommonRealBufferedSinkTest.kt202
-rw-r--r--okio/src/commonTest/kotlin/okio/CommonRealBufferedSourceTest.kt156
-rw-r--r--okio/src/commonTest/kotlin/okio/FakeClock.kt32
-rw-r--r--okio/src/commonTest/kotlin/okio/HashingSinkTest.kt119
-rw-r--r--okio/src/commonTest/kotlin/okio/HashingSourceTest.kt128
-rw-r--r--okio/src/commonTest/kotlin/okio/HashingTest.kt143
-rw-r--r--okio/src/commonTest/kotlin/okio/MockSink.kt63
-rw-r--r--okio/src/commonTest/kotlin/okio/UnsafeCursorTest.kt87
-rw-r--r--okio/src/commonTest/kotlin/okio/Utf8KotlinTest.kt189
-rw-r--r--okio/src/commonTest/kotlin/okio/util.kt94
-rw-r--r--okio/src/hashFunctions/kotlin/okio/internal/HashFunction.kt27
-rw-r--r--okio/src/hashFunctions/kotlin/okio/internal/Hmac.kt75
-rw-r--r--okio/src/hashFunctions/kotlin/okio/internal/Md5.kt206
-rw-r--r--okio/src/hashFunctions/kotlin/okio/internal/Sha1.kt204
-rw-r--r--okio/src/hashFunctions/kotlin/okio/internal/Sha256.kt253
-rw-r--r--okio/src/hashFunctions/kotlin/okio/internal/Sha512.kt291
-rw-r--r--okio/src/jvmMain/kotlin/okio/-DeprecatedOkio.kt147
-rw-r--r--okio/src/jvmMain/kotlin/okio/-DeprecatedUpgrade.kt20
-rw-r--r--okio/src/jvmMain/kotlin/okio/-DeprecatedUtf8.kt40
-rw-r--r--okio/src/jvmMain/kotlin/okio/-Platform.kt37
-rw-r--r--okio/src/jvmMain/kotlin/okio/AsyncTimeout.kt328
-rw-r--r--okio/src/jvmMain/kotlin/okio/Buffer.kt622
-rw-r--r--okio/src/jvmMain/kotlin/okio/BufferedSink.kt105
-rw-r--r--okio/src/jvmMain/kotlin/okio/BufferedSource.kt163
-rw-r--r--okio/src/jvmMain/kotlin/okio/ByteString.kt354
-rw-r--r--okio/src/jvmMain/kotlin/okio/CipherSink.kt129
-rw-r--r--okio/src/jvmMain/kotlin/okio/CipherSource.kt113
-rw-r--r--okio/src/jvmMain/kotlin/okio/DeflaterSink.kt161
-rw-r--r--okio/src/jvmMain/kotlin/okio/ForwardingSink.kt48
-rw-r--r--okio/src/jvmMain/kotlin/okio/ForwardingSource.kt45
-rw-r--r--okio/src/jvmMain/kotlin/okio/ForwardingTimeout.kt52
-rw-r--r--okio/src/jvmMain/kotlin/okio/GzipSink.kt152
-rw-r--r--okio/src/jvmMain/kotlin/okio/GzipSource.kt219
-rw-r--r--okio/src/jvmMain/kotlin/okio/HashingSink.kt139
-rw-r--r--okio/src/jvmMain/kotlin/okio/HashingSource.kt150
-rw-r--r--okio/src/jvmMain/kotlin/okio/InflaterSource.kt146
-rw-r--r--okio/src/jvmMain/kotlin/okio/JvmOkio.kt235
-rw-r--r--okio/src/jvmMain/kotlin/okio/Pipe.kt249
-rw-r--r--okio/src/jvmMain/kotlin/okio/RealBufferedSink.kt140
-rw-r--r--okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt181
-rw-r--r--okio/src/jvmMain/kotlin/okio/SegmentPool.kt127
-rw-r--r--okio/src/jvmMain/kotlin/okio/SegmentedByteString.kt131
-rw-r--r--okio/src/jvmMain/kotlin/okio/Sink.kt33
-rw-r--r--okio/src/jvmMain/kotlin/okio/Throttler.kt168
-rw-r--r--okio/src/jvmMain/kotlin/okio/Timeout.kt233
-rw-r--r--okio/src/jvmMain/resources/META-INF/proguard/okio.pro2
-rw-r--r--okio/src/jvmTest/java/okio/AsyncTimeoutTest.java392
-rw-r--r--okio/src/jvmTest/java/okio/BufferCursorTest.java468
-rw-r--r--okio/src/jvmTest/java/okio/BufferTest.java580
-rw-r--r--okio/src/jvmTest/java/okio/BufferedSinkJavaTest.java245
-rw-r--r--okio/src/jvmTest/java/okio/BufferedSinkTest.java380
-rw-r--r--okio/src/jvmTest/java/okio/BufferedSourceJavaTest.java232
-rw-r--r--okio/src/jvmTest/java/okio/BufferedSourceTest.java1492
-rw-r--r--okio/src/jvmTest/java/okio/ByteStringJavaTest.java633
-rw-r--r--okio/src/jvmTest/java/okio/DeflaterSinkTest.java149
-rw-r--r--okio/src/jvmTest/java/okio/ForwardingTimeoutTest.java46
-rw-r--r--okio/src/jvmTest/java/okio/GzipSinkTest.java61
-rw-r--r--okio/src/jvmTest/java/okio/GzipSourceTest.java233
-rw-r--r--okio/src/jvmTest/java/okio/InflaterSourceTest.java210
-rw-r--r--okio/src/jvmTest/java/okio/LargeStreamsTest.java118
-rw-r--r--okio/src/jvmTest/java/okio/MessageDigestConsistencyTest.kt100
-rw-r--r--okio/src/jvmTest/java/okio/NioTest.java147
-rw-r--r--okio/src/jvmTest/java/okio/OkioTest.java169
-rw-r--r--okio/src/jvmTest/java/okio/PipeTest.java376
-rw-r--r--okio/src/jvmTest/java/okio/ReadUtf8LineTest.java212
-rw-r--r--okio/src/jvmTest/java/okio/SocketTimeoutTest.java138
-rw-r--r--okio/src/jvmTest/java/okio/Utf8Test.java282
-rw-r--r--okio/src/jvmTest/java/okio/WaitUntilNotifiedTest.java172
-rw-r--r--okio/src/jvmTest/java/okio/internal/HmacTest.kt111
-rw-r--r--okio/src/jvmTest/kotlin/okio/BufferCursorKotlinTest.kt129
-rw-r--r--okio/src/jvmTest/kotlin/okio/BufferFactory.kt66
-rw-r--r--okio/src/jvmTest/kotlin/okio/BufferKotlinTest.kt95
-rw-r--r--okio/src/jvmTest/kotlin/okio/ByteStringKotlinTest.kt66
-rw-r--r--okio/src/jvmTest/kotlin/okio/CipherAlgorithm.kt63
-rw-r--r--okio/src/jvmTest/kotlin/okio/CipherFactory.kt36
-rw-r--r--okio/src/jvmTest/kotlin/okio/CipherSinkTest.kt196
-rw-r--r--okio/src/jvmTest/kotlin/okio/CipherSourceTest.kt187
-rw-r--r--okio/src/jvmTest/kotlin/okio/DeflateKotlinTest.kt51
-rw-r--r--okio/src/jvmTest/kotlin/okio/ForwardingTimeoutKotlinTest.kt45
-rw-r--r--okio/src/jvmTest/kotlin/okio/GzipKotlinTest.kt36
-rw-r--r--okio/src/jvmTest/kotlin/okio/OkioKotlinTest.kt126
-rw-r--r--okio/src/jvmTest/kotlin/okio/PipeKotlinTest.kt883
-rw-r--r--okio/src/jvmTest/kotlin/okio/SegmentSharingTest.kt175
-rw-r--r--okio/src/jvmTest/kotlin/okio/Stopwatch.kt34
-rw-r--r--okio/src/jvmTest/kotlin/okio/TestUtil.kt302
-rw-r--r--okio/src/jvmTest/kotlin/okio/ThrottlerTakeTest.kt142
-rw-r--r--okio/src/jvmTest/kotlin/okio/ThrottlerTest.kt161
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/-Platform.kt46
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/Buffer.kt322
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/BufferedSink.kt60
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/BufferedSource.kt98
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/ByteString.kt180
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/HashingSink.kt88
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/HashingSource.kt92
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/RealBufferedSink.kt75
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/RealBufferedSource.kt114
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/SegmentPool.kt27
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/SegmentedByteString.kt95
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/Sink.kt29
-rw-r--r--okio/src/nonJvmMain/kotlin/okio/Timeout.kt22
-rw-r--r--samples/build.gradle23
-rw-r--r--samples/src/jvmMain/java/okio/samples/BitmapEncoder.java127
-rw-r--r--samples/src/jvmMain/java/okio/samples/ByteChannelSink.java70
-rw-r--r--samples/src/jvmMain/java/okio/samples/ByteChannelSource.java70
-rw-r--r--samples/src/jvmMain/java/okio/samples/ExploreCharsets.java40
-rw-r--r--samples/src/jvmMain/java/okio/samples/FileChannelSink.java66
-rw-r--r--samples/src/jvmMain/java/okio/samples/FileChannelSource.java58
-rw-r--r--samples/src/jvmMain/java/okio/samples/GoldenValue.java73
-rw-r--r--samples/src/jvmMain/java/okio/samples/Hashing.java90
-rw-r--r--samples/src/jvmMain/java/okio/samples/Interceptors.java161
-rw-r--r--samples/src/jvmMain/java/okio/samples/Randoms.java80
-rw-r--r--samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java48
-rw-r--r--samples/src/jvmMain/java/okio/samples/SocksProxyServer.java199
-rw-r--r--samples/src/jvmMain/java/okio/samples/SourceMarker.java185
-rw-r--r--samples/src/jvmMain/java/okio/samples/WriteFile.java47
-rw-r--r--samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt113
-rw-r--r--samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt35
-rw-r--r--samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt71
-rw-r--r--samples/src/jvmMain/kotlin/okio/samples/Hashing.kt91
-rw-r--r--samples/src/jvmMain/kotlin/okio/samples/ReadFileLineByLine.kt39
-rw-r--r--samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt189
-rw-r--r--samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt37
-rw-r--r--samples/src/jvmTest/java/okio/samples/ChannelsTest.java134
-rw-r--r--samples/src/jvmTest/java/okio/samples/SourceMarkerTest.java243
-rw-r--r--settings.gradle14
221 files changed, 34926 insertions, 0 deletions
diff --git a/.buildscript/prepare_mkdocs.sh b/.buildscript/prepare_mkdocs.sh
new file mode 100755
index 00000000..5dcb42cd
--- /dev/null
+++ b/.buildscript/prepare_mkdocs.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# The website is built using MkDocs with the Material theme.
+# https://squidfunk.github.io/mkdocs-material/
+# It requires Python to run.
+# Install the packages with the following command:
+# pip install mkdocs mkdocs-material
+
+set -ex
+
+# Generate the API docs
+./gradlew okio:dokkaHtml
+
+# Copy in special files that GitHub wants in the project root.
+cp CHANGELOG.md docs/changelog.md
+cp CONTRIBUTING.md docs/contributing.md
diff --git a/.buildscript/restore_v1_docs.sh b/.buildscript/restore_v1_docs.sh
new file mode 100644
index 00000000..220fed82
--- /dev/null
+++ b/.buildscript/restore_v1_docs.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# Commit b3205fa199a19d6fbf13ee5c8e0c3d6d2b15b05f contains
+# Javadoc for Okio 1.x. Those should be present on
+# gh-pages and published along with the other website
+# content, but if for some reason they have to be re-added
+# to gh-pages - run this script locally.
+
+set -ex
+
+REPO="git@github.com:square/okio.git"
+DIR=temp-clone
+
+# Delete any existing temporary website clone
+rm -rf $DIR
+
+# Clone the current repo into temp folder
+git clone $REPO $DIR
+
+# Move working directory into temp folder
+cd $DIR
+
+# Restore Javadocs from 1.x
+git checkout gh-pages
+git cherry-pick b3205fa199a19d6fbf13ee5c8e0c3d6d2b15b05f
+git push
+
+# Delete our temp folder
+cd ..
+rm -rf $DIR
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..15d8bd6e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,5 @@
+[*.kt]
+indent_size = 2
+
+[*.gradle]
+indent_size = 2
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..26b26894
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,162 @@
+name: build
+
+on: [push, pull_request]
+
+env:
+ GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false"
+
+jobs:
+ jvm:
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ java-version:
+ - 1.8
+ - 9
+ - 10
+ - 11
+ - 12
+ - 13
+ - 14
+ - 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: Configure JDK
+ uses: actions/setup-java@v1
+ with:
+ java-version: ${{ matrix.java-version }}
+
+ - name: Test
+ run: |
+ ./gradlew -Dkjs=false -Dknative=false build
+
+ - name: Upload Japicmp report
+ if: failure()
+ uses: actions/upload-artifact@master
+ with:
+ name: japicmp-report
+ path: okio/jvm/japicmp/build/reports/japi.txt
+
+ multiplatform:
+ runs-on: macOS-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: Configure JDK
+ uses: actions/setup-java@v1
+ with:
+ java-version: 14
+
+ - name: Test
+ run: |
+ ./gradlew build
+
+ windows:
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/wrapper-validation-action@v1
+
+ - name: Configure JDK
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+
+ - name: Test
+ run: |
+ ./gradlew build
+
+ publish:
+ runs-on: macOS-latest
+ if: github.ref == 'refs/heads/master'
+ needs: [jvm, multiplatform, windows]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Configure JDK
+ uses: actions/setup-java@v1
+ with:
+ java-version: 14
+
+ - name: Upload Artifacts
+ run: |
+ ./gradlew clean publish
+ env:
+ ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
+ ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
+
+ publish-windows:
+ runs-on: windows-latest
+ if: github.ref == 'refs/heads/master'
+ needs: [jvm, multiplatform, windows]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Configure JDK
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+
+ - name: Upload Artifacts
+ run: |
+ ./gradlew clean publishMingwX64PublicationToMavenRepository
+ env:
+ ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
+ ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
+
+ publish-website:
+ runs-on: ubuntu-latest
+ if: github.ref == 'refs/heads/master'
+ needs: [jvm, multiplatform]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Configure JDK
+ uses: actions/setup-java@v1
+ with:
+ java-version: 14
+
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: Prepare docs
+ run: .buildscript/prepare_mkdocs.sh
+
+ - name: Build mkdocs
+ run: |
+ pip3 install mkdocs-macros-plugin
+ mkdocs build
+
+ - name: Deploy docs
+ if: success()
+ uses: JamesIves/github-pages-deploy-action@releases/v3
+ with:
+ GITHUB_TOKEN: ${{ secrets.GH_CLIPPY_TOKEN }}
+ BRANCH: gh-pages
+ FOLDER: site
+ SINGLE_COMMIT: true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..451ec21b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,30 @@
+.classpath
+.gradle
+.project
+.settings
+eclipsebin
+
+bin
+gen
+build
+out
+lib
+reports
+
+.idea
+*.iml
+*.ipr
+*.iws
+classes
+local.properties
+
+obj
+
+.DS_Store
+
+node_modules
+
+# Special Mkdocs files
+docs/2.x
+docs/changelog.md
+docs/contributing.md
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 00000000..cf49f1c0
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,15 @@
+java_library {
+ name: "okio-lib",
+ srcs: [
+ "okio/src/jvmMain/**/*.kt",
+ "okio/src/commonMain/**/*.kt",
+ ],
+ static_libs: [
+ "guava-android-annotation-stubs",
+ ],
+ kotlincflags: [
+ "-Xmulti-platform",
+ ],
+ sdk_version: "current",
+ java_version: "1.7",
+}
diff --git a/BUG-BOUNTY.md b/BUG-BOUNTY.md
new file mode 100644
index 00000000..c8c3b94f
--- /dev/null
+++ b/BUG-BOUNTY.md
@@ -0,0 +1,10 @@
+Serious about security
+======================
+
+Square recognizes the important contributions the security research community
+can make. We therefore encourage reporting security issues with the code
+contained in this repository.
+
+If you believe you have discovered a security vulnerability, please follow the
+guidelines at https://bugcrowd.com/squareopensource
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..48028d0a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,583 @@
+Change Log
+==========
+
+## Version 2.9.0
+
+_2020-10-04_
+
+ * Fix: Don't corrupt the `Buffer` when writing a slice of a segmented `ByteString`. We had a severe
+ bug where `ByteString` instances created with `snapshot()` and `readByteString()` incorrectly
+ adjusted the buffer's size by their full length, not the length of the slice. This would have
+ caused buffer reads to crash! We do not believe data was silently corrupted.
+ * New: `CipherSink` and `CipherSource`. Use these with `javax.crypto.Cipher` to encrypt and decrypt
+ streams of data. This is a low-level encryption API; most applications should use higher-level
+ APIs like TLS when available.
+ * New: Promote hash functions `md5`, `sha1()`, `sha512()`, and `sha256()` to common Kotlin. These
+ are currently only available on `ByteString`, multiplatform support for `HashingSource`,
+ `HashingSink`, and `Buffer` should come in a follow-up release. We wrote and optimized our own
+ implementations of these hash functions in Kotlin. On JVM and Android platforms Okio still uses
+ the platform's built-in hash functions.
+ * New: Support OSGi metadata.
+ * Upgrade: [Kotlin 1.4.10][kotlin_1_4_10].
+
+
+## Version 2.8.0
+
+_2020-08-17_
+
+ * New: Upgrade to Kotlin 1.4.0.
+
+
+## Version 2.7.0
+
+_2020-07-07_
+
+ * New: `Pipe.cancel()` causes in-progress and future reads and writes on the pipe to immediately
+ fail with an `IOException`. The streams may still be canceled normally.
+
+ * New: Enlarge Okio's internal segment pool from a fixed 64 KiB total to 64 KiB per processor. For
+ example, on an Intel i9 8-core/16-thread machine the segment pool now uses up to 1 MiB of memory.
+
+ * New: Migrate from `synchronized` to lock-free when accessing the segment pool. Combined with the
+ change above we saw throughput increase 3x on a synthetic benchmark designed to create
+ contention.
+
+
+## Version 2.6.0
+
+_2020-04-22_
+
+ * New: `InflaterSource.readOrInflate()` is like `InflaterSource.read()`, except it will return 0 if
+ consuming deflated bytes from the underlying stream did not produce new inflated bytes.
+
+
+## Version 2.5.0
+
+_2020-03-20_
+
+ * New: Upgrade to Kotlin 1.3.70.
+
+
+## Version 2.4.3
+
+_2019-12-20_
+
+ * New: Upgrade to Kotlin 1.3.61.
+
+
+## Version 2.4.2
+
+_2019-12-11_
+
+ * Fix: Don't crash when an `InputStream` source is exhausted exactly at a buffer segment boundary.
+ We had a bug where a sequence of reads could violate a buffer's invariants, and this could result
+ in a crash when subsequent reads encountered an unexpected empty segment.
+
+
+## Version 1.17.5
+
+_2019-12-11_
+
+ * Fix: Don't crash when an `InputStream` source is exhausted exactly at a buffer segment boundary.
+ We had a bug where a sequence of reads could violate a buffer's invariants, and this could result
+ in a crash when subsequent reads encountered an unexpected empty segment.
+
+
+### Version 2.4.1
+
+_2019-10-04_
+
+ * Fix: Don't cache hash code and UTF-8 string in `ByteString` on Kotlin/Native which prevented freezing.
+
+### Version 2.4.0
+
+_2019-08-26_
+
+ * New: Upgrade to Kotlin 1.3.50.
+
+
+### Version 2.3.0
+
+_2019-07-29_
+
+**This release changes our build from Kotlin-JVM to Kotlin-multiplatform (which includes JVM).**
+Both native and JavaScript platforms are unstable preview releases and subject to
+backwards-incompatible changes in forthcoming releases.
+
+To try Okio in a multiplatform project use this Maven coordinate:
+
+```kotlin
+api('com.squareup.okio:okio-multiplatform:2.3.0')
+```
+
+You’ll also need to [enable Gradle metadata][gradle_metadata] in your project's settings. The
+artifact name for JVM projects has not changed.
+
+ * New: Upgrade to Kotlin 1.3.40.
+ * Fix: Use Gradle `api` instead of `implementation` for the kotlin-stdlib dependency.
+ * Fix: Don't block unless strictly necessary in `BufferedSource.peek()`.
+
+## Version 1.17.4
+
+_2019-04-29_
+
+ * Fix: Don't block unless strictly necessary in `BufferedSource.peek()`.
+
+
+## Version 2.2.2
+
+_2019-01-28_
+
+ * Fix: Make `Pipe.fold()` close the underlying sink when necessary.
+
+
+## Version 1.17.3
+
+_2019-01-28_
+
+ * Fix: Make `Pipe.fold()` close the underlying sink when necessary.
+
+
+## Version 1.17.2
+
+_2019-01-17_
+
+ * Fix: Make `Pipe.fold()` flush the underlying sink.
+
+
+## Version 2.2.1
+
+_2019-01-17_
+
+ * Fix: Make `Pipe.fold()` flush the underlying sink.
+
+
+## Version 2.2.0
+
+_2019-01-16_
+
+ * New: `Throttler` limits sources and sinks to a maximum desired throughput. Multiple sources and
+ sinks can be attached to the same throttler and their combined throughput will not exceed the
+ desired throughput. Multiple throttlers can also be used on the same source or sink and they will
+ all be honored.
+
+ * New: `Pipe.fold()` replaces the actively-readable `Source` with a passively-writable `Sink`.
+ This can be used to forward one sink to a target that is initially undetermined.
+
+ * New: Optimize performance of ByteStrings created with `Buffer.snapshot()`.
+
+
+## Version 1.17.1
+
+_2019-01-16_
+
+ * Fix: Make the newly-backported `Pipe.fold()` public.
+
+
+## Version 1.17.0
+
+_2019-01-16_
+
+ * New: Backport `Pipe.fold()` to Okio 1.x.
+
+
+## Version 1.16.0
+
+_2018-10-08_
+
+ * New: Backport `BufferedSource.peek()` and `BufferedSource.getBuffer()` to Okio 1.x.
+ * Fix: Enforce timeouts when closing `AsyncTimeout` sources.
+
+
+## Version 2.1.0
+
+_2018-09-22_
+
+ * New: `BufferedSource.peek()` returns another `BufferedSource` that reads ahead on the current
+ source. Use this to process the same data multiple times.
+
+ * New: Deprecate `BufferedSource.buffer()`, replacing it with either `BufferedSource.getBuffer()`
+ (in Java) or `BufferedSource.buffer` (in Kotlin). We have done likewise for `BufferedSink`.
+ When we introduced the new extension method `Source.buffer()` in Okio 2.0 we inadvertently
+ collided with an existing method. This fixes that.
+
+ * New: Improve performance of `Buffer.writeUtf8()`. This comes alongside initial implementation of
+ UTF-8 encoding and decoding in JavaScript which [uses XOR masks][xor_utf8] for great performance.
+
+
+## Version 2.0.0
+
+_2018-08-27_
+
+This release commits to a stable 2.0 API. Read the 2.0.0-RC1 changes for advice on upgrading from
+1.x to 2.x.
+
+We've also added APIs to ease migration for Kotlin users. They use Kotlin's `@Deprecated` annotation
+to help you change call sites from the 1.x style to the 2.x style.
+
+
+## Version 2.0.0-RC1
+
+_2018-07-26_
+
+Okio 2 is a major release that upgrades the library's implementation language from Java to Kotlin.
+
+Okio 2.x is **binary-compatible** with Okio 1.x and does not change any behavior. Classes and .jar
+files compiled against 1.x can be used with 2.x without recompiling.
+
+Okio 2.x is **.java source compatible** with Okio 1.x in all but one corner case. In Okio 1.x
+`Buffer` would throw an unchecked `IllegalStateException` when attempting to read more bytes than
+available. Okio 2.x now throws a checked `EOFException` in this case. This is now consistent with
+the behavior of its `BufferedSource` interface. Java callers that don't already catch `IOException`
+will now need to.
+
+Okio 2.x is **.kt source-incompatible** with Okio 1.x. This release adopts Kotlin idioms where they
+are available.
+
+| Java | Kotlin | Idiom |
+| :--------------------------------------- | :---------------------------------- | :----------------- |
+| Buffer.getByte() | operator fun Buffer.get() | operator function |
+| Buffer.size() | val Buffer.size | val |
+| ByteString.decodeBase64(String) | fun String.decodeBase64() | extension function |
+| ByteString.decodeHex(String) | fun String.decodeHex() | extension function |
+| ByteString.encodeString(String, Charset) | fun String.encode(Charset) | extension function |
+| ByteString.encodeUtf8(String) | fun String.encodeUtf8() | extension function |
+| ByteString.getByte() | operator fun ByteString.get() | operator function |
+| ByteString.of(ByteBuffer) | fun ByteBuffer.toByteString() | extension function |
+| ByteString.of(byte[], int, int) | fun ByteArray.toByteString() | extension function |
+| ByteString.read(InputStream, int) | fun InputStream.readByteString(Int) | extension function |
+| ByteString.size() | val ByteString.size | val |
+| DeflaterSink(Sink) | fun Sink.deflater() | extension function |
+| ForwardingSink.delegate() | val ForwardingSink.delegate | val |
+| ForwardingSource.delegate() | val ForwardingSource.delegate | val |
+| GzipSink(Sink, Deflater) | fun Sink.gzip() | extension function |
+| GzipSink.deflater() | val GzipSink.deflater | val |
+| GzipSource(Source) | fun Source.gzip() | extension function |
+| HashingSink.hash() | val HashingSink.hash | val |
+| HashingSource.hash() | val HashingSource.hash | val |
+| InflaterSink(Source) | fun Source.inflater() | extension function |
+| Okio.appendingSink(File) | fun File.appendingSink() | extension function |
+| Okio.blackhole() | fun blackholeSink() | top level function |
+| Okio.buffer(Sink) | fun Sink.buffer() | extension function |
+| Okio.buffer(Source) | fun Source.buffer() | extension function |
+| Okio.sink(File) | fun File.sink() | extension function |
+| Okio.sink(OutputStream) | fun OutputStream.sink() | extension function |
+| Okio.sink(Path) | fun Path.sink() | extension function |
+| Okio.sink(Socket) | fun Socket.sink() | extension function |
+| Okio.source(File) | fun File.source() | extension function |
+| Okio.source(InputStream) | fun InputStream.source() | extension function |
+| Okio.source(Path) | fun Path.source() | extension function |
+| Okio.source(Socket) | fun Socket.source() | extension function |
+| Pipe.sink() | val Pipe.sink | val |
+| Pipe.source() | val Pipe.source | val |
+| Utf8.size(String) | fun String.utf8Size() | extension function |
+
+Okio 2.x has **similar performance** to Okio 1.x. We benchmarked both versions to find potential
+performance regressions. We found one regression and fixed it: we were using `==` instead of `===`.
+
+Other changes in this release:
+
+ * New: Add a dependency on kotlin-stdlib. Okio's transitive dependencies grow from none in 1.x to
+ three in 2.x. These are kotlin-stdlib (939 KiB), kotlin-stdlib-common (104 KiB), and JetBrains'
+ annotations (17 KiB).
+
+ * New: Change Okio to build with Gradle instead of Maven.
+
+
+## Version 1.15.0
+
+_2018-07-18_
+
+ * New: Trie-based `Buffer.select()`. This improves performance when selecting
+ among large lists of options.
+ * Fix: Retain interrupted state when throwing `InterruptedIOException`.
+
+
+## Version 1.14.0
+
+_2018-02-11_
+
+ * New: `Buffer.UnsafeCursor` provides direct access to Okio internals. This API
+ is like Okio's version of Java reflection: it's a very powerful API that can
+ be used for great things and dangerous things alike. The documentation is
+ extensive and anyone using it should review it carefully before proceeding!
+ * New: Change `BufferedSource` to implement `java.nio.ReadableByteChannel` and
+ `BufferedSink` to implement `java.nio.WritableByteChannel`. Now it's a little
+ easier to interop between Okio and NIO.
+ * New: Automatic module name of `okio` for use with the Java Platform Module
+ System.
+ * New: Optimize `Buffer.getByte()` to search backwards when doing so will be
+ more efficient.
+ * Fix: Honor the requested byte count in `InflaterSource`. Previously this
+ class could return more bytes than requested.
+ * Fix: Improve a performance bug in `AsyncTimeout.sink().write()`.
+
+
+## Version 1.13.0
+
+_2017-05-12_
+
+ * **Okio now uses `@Nullable` to annotate all possibly-null values.** We've
+ added a compile-time dependency on the JSR 305 annotations. This is a
+ [provided][maven_provided] dependency and does not need to be included in
+ your build configuration, `.jar` file, or `.apk`. We use
+ `@ParametersAreNonnullByDefault` and all parameters and return types are
+ never null unless explicitly annotated `@Nullable`.
+
+ * **Warning: this release is source-incompatible for Kotlin users.**
+ Nullability was previously ambiguous and lenient but now the compiler will
+ enforce strict null checks.
+
+
+## Version 1.12.0
+
+_2017-04-11_
+
+ * **Fix: Change Pipe's sink.flush() to not block.** Previously closing a pipe's
+ sink would block until the source had been exhausted. In practice this
+ blocked the caller for no benefit.
+ * **Fix: Change `writeUtf8CodePoint()` to emit `?` for partial surrogates.**
+ The previous behavior was inconsistent: given a malformed string with a
+ partial surrogate, `writeUtf8()` emitted `?` but `writeUtf8CodePoint()` threw
+ an `IllegalArgumentException`. Most applications will never encounter partial
+ surrogates, but for those that do this behavior was unexpected.
+ * New: Allow length of `readUtf8LineStrict()` to be limited.
+ * New: `Utf8.size()` method to get the number of bytes required to encode a
+ string as UTF-8. This may be useful for length-prefixed encodings.
+ * New: SHA-512 hash and HMAC APIs.
+
+
+## Version 1.11.0
+
+_2016-10-11_
+
+ * **Fix: The four-argument overload of `Buffer.writeString()` had a major bug
+ where it didn't respect offsets if the specified charset was UTF-8.** This
+ was because our short-circuit optimization omitted necessary offset
+ parameters.
+ * New: HMAC support in `HashingSource`, `HashingSink`, `ByteString`, and
+ `Buffer`. This makes it easy to create a keyed-hash message authentication
+ code (HMAC) wherever your data is. Unlike the other hashes, HMAC uses a
+ `ByteString` secret key for authentication.
+ * New: `ByteString.of(ByteBuffer)` makes it easier to mix NIO with Okio.
+
+
+## Version 1.10.0
+
+_2016-08-28_
+
+ * Fix: Support reading files larger than 2 GiB with `GzipSource`. Previously
+ attempting to decompress such files would fail due to an overflow when
+ validating the total length.
+ * Fix: Exit the watchdog thread after being idle for 60 seconds. This should
+ make it possible for class unloaders to fully unload Okio.
+ * New: `Okio.blackhole()` returns a sink where all bytes written are discarded.
+ This is Okio's equivalent of `/dev/null`.
+ * New: Encode a string with any charset using `ByteString.encodeString()` and
+ decode strings in any charset using `ByteString.string()`. Most applications
+ should prefer `ByteString.encodeUtf8()` and `ByteString.utf8()` unless it's
+ necessary to support a legacy charset.
+ * New: `GzipSink.deflater()` makes it possible to configure the compression
+ level.
+
+
+## Version 1.9.0
+
+_2016-07-01_
+
+ * New: `Pipe` makes it easy to connect a producer thread to a consumer thread.
+ Reads block until data is available to read. Writes block if the pipe's is
+ full. Both sources and sinks support timeouts.
+ * New: `BufferedSource.rangeEquals()` makes it easy to compare a range in a
+ stream to an expected value. This does the right thing: it blocks to load
+ the data required return a definitive result. But it won't block
+ unnecessarily.
+ * New: `Timeout.waitUntilNotified()` makes it possible to use nice timeout
+ abstractions on Java's built-in wait/notify primitives.
+ * Fix: Don't return incorrect results when `HashingSource` does large reads.
+ There was a bug where it wasn't traversing through the segments of the buffer
+ being hashed. This means that `HashingSource` was returning incorrect answers
+ for any writes that spanned multiple segment boundaries.
+
+## Version 1.8.0
+
+_2016-05-02_
+
+ * New: `BufferedSource.select(Options)` API for reading one of a set of
+ expected values.
+ * New: Make `ByteString.toString()` and `Buffer.toString()` friendlier.
+ These methods return text if the byte string is valid UTF-8.
+ * New: APIs to match byte strings: `indexOf()`, `startsWith()`, and
+ `endsWith()`.
+
+## Version 1.7.0
+
+_2016-04-10_
+
+ * New: Change the segment size to 8 KiB. This has been reported to dramatically
+ improve performance in some applications.
+ * New: `md5()`, `sha1()`, and `sha256()` methods on `Buffer`. Also add a
+ `sha1()` method on `ByteString` for symmetry.
+ * New: `HashingSource` and `HashingSink`. These classes are Okio’s equivalent
+ to the JDK’s `DigestInputStream` and `DigestOutputStream`. They offer
+ convenient `md5()`, `sha1()`, and `sha256()` factory methods to avoid an
+ impossible `NoSuchAlgorithmException`.
+ * New: `ByteString.asByteBuffer()`.
+ * Fix: Limit snapshot byte strings to requested size.
+ * Fix: Change write timeouts to have a maximum write size. Previously large
+ writes could easly suffer timeouts because the entire write was subject to a
+ single timeout.
+ * Fix: Recover from EBADF failures, which could be triggered by asynchronously
+ closing a stream on older versions of Android.
+ * Fix: Don't share segments if doing so only saves a small copy. This should
+ improve performance for all applications.
+ * Fix: Optimize `BufferedSource.indexOfElement()` and `indexOf(ByteString)`.
+ Previously this method had a bug that caused it to be very slow on large
+ buffers.
+
+## Version 1.6.0
+
+_2015-08-25_
+
+ * New: `BufferedSource.indexOf(ByteString)` searches a source for the next
+ occurrence of a byte string.
+ * Fix: Recover from unexpected `AssertionError` thrown on Android 4.2.2 and
+ earlier when asynchronously closing a socket.
+
+## Version 1.5.0
+
+_2015-06-19_
+
+ * Sockets streams now throw `SocketTimeoutException`. This builds on new
+ extension point in `AsyncTimeout` to customize the exception when a timeout
+ occurs.
+ * New: `ByteString` now implements `Comparable`. The comparison sorts bytes as
+ unsigned: {@code ff} sorts after {@code 00}.
+
+## Version 1.4.0
+
+_2015-05-16_
+
+ * **Timeout exception changed.** Previously `Timeout.throwIfReached()` would
+ throw `InterruptedIOException` on thread interruption, and `IOException` if
+ the deadline was reached. Now it throws `InterruptedIOException` in both
+ cases.
+ * Fix: throw `EOFException` when attempting to read digits from an empty
+ source. Previously this would crash with an unchecked exception.
+ * New: APIs to read and write UTF-8 code points without allocating strings.
+ * New: `BufferedSink` can now write substrings directly, potentially saving an
+ allocation for some callers.
+ * New: `ForwardingTimeout` class.
+
+## Version 1.3.0
+
+_2015-03-16_
+
+ * New: Read and write signed decimal and unsigned hexadecimal values in
+ `BufferedSource` and `BufferedSink`. Unlike the alternatives, these methods
+ don’t do any memory allocations!
+ * New: Segment sharing. This improves the runtime of operations like
+ `Buffer.clone()` and `Buffer.copyTo()` by sharing underlying segments between
+ buffers.
+ * New: `Buffer.snapshot()` returns an immutable snapshot of a buffer as a
+ `ByteString`. This builds on segment sharing so that snapshots are shallow,
+ immutable copies.
+ * New: `ByteString.rangeEquals()`.
+ * New: `ByteString.md5()` and `ByteString.sha256()`.
+ * New: `ByteString.base64Url()` returns URL-safe Base64. The existing
+ decoding method has been extended to support URL-safe Base64 input.
+ * New: `ByteString.substring()` returns a prefix, infix, or suffix.
+ * New: `Sink` now implements `java.io.Flushable`.
+ * Fix: `Buffer.write(Source, long)` now always writes fully. The previous
+ behavior would return as soon as any data had been written; this was
+ inconsistent with all other _write()_ methods in the API.
+ * Fix: don't leak empty segments in DeflaterSink and InflaterSource. (This was
+ unlikely to cause problems in practice.)
+
+## Version 1.2.0
+
+_2014-12-30_
+
+ * Fix: `Okio.buffer()` _always_ buffers for better predictability.
+ * Fix: Provide context when `readUtf8LineStrict()` throws.
+ * Fix: Buffers do not call through the `Source` on zero-byte writes.
+
+## Version 1.1.0
+
+_2014-12-11_
+
+ * Do UTF-8 encoding natively for a performance increase, particularly on Android.
+ * New APIs: `BufferedSink.emit()`, `BufferedSource.request()` and `BufferedSink.indexOfElement()`.
+ * Fixed a performance bug in `Buffer.indexOf()`
+
+## Version 1.0.1
+
+_2014-08-08_
+
+ * Added `read(byte[])`, `read(byte[], offset, byteCount)`, and
+ `void readFully(byte[])` to `BufferedSource`.
+ * Refined declared checked exceptions on `Buffer` methods.
+
+
+## Version 1.0.0
+
+_2014-05-23_
+
+ * Bumped release version. No other changes!
+
+## Version 0.9.0
+
+_2014-05-03_
+
+ * Use 0 as a sentinel for no timeout.
+ * Make AsyncTimeout public.
+ * Remove checked exception from Buffer.readByteArray.
+
+## Version 0.8.0
+
+_2014-04-24_
+
+ * Eagerly verify preconditions on public APIs.
+ * Quick return on Buffer instance equivalence.
+ * Add delegate types for Sink and Source.
+ * Small changes to the way deadlines are managed.
+ * Add append variant of Okio.sink for File.
+ * Methods to exhaust BufferedSource to byte[] and ByteString.
+
+## Version 0.7.0
+
+_2014-04-18_
+
+ * Don't use getters in timeout.
+ * Use the watchdog to interrupt sockets that have reached deadlines.
+ * Add java.io and java.nio file source/sink helpers.
+
+## Version 0.6.1
+
+_2014-04-17_
+
+ * Methods to read a buffered source fully in UTF-8 or supplied charset.
+ * API to read a byte[] directly.
+ * New methods to move all data from a source to a sink.
+ * Fix a bug on input stream exhaustion.
+
+## Version 0.6.0
+
+_2014-04-15_
+
+ * Make ByteString serializable.
+ * New API: `ByteString.of(byte[] data, int offset, int byteCount)`
+ * New API: stream-based copy, write, and read helpers.
+
+## Version 0.5.0
+
+_2014-04-08_
+
+ * Initial public release.
+ * Imported from OkHttp.
+
+
+ [gradle_metadata]: https://blog.gradle.org/gradle-metadata-1.0
+ [kotlin_1_4_10]: https://github.com/JetBrains/kotlin/releases/tag/v1.4.10
+ [maven_provided]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
+ [xor_utf8]: https://github.com/square/okio/blob/bbb29c459e5ccf0f286e0b17ccdcacd7ac4bc2a9/okio/src/main/kotlin/okio/Utf8.kt#L302
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..ac16ed7e
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,38 @@
+Contributing
+============
+
+Keeping the project small and stable limits our ability to accept new contributors. We are not
+seeking new committers at this time, but some small contributions are welcome.
+
+If you've found a security problem, please follow our [bug bounty][security] program.
+
+If you've found a bug, please contribute a failing test case so we can study and fix it.
+
+Before code can be accepted all contributors must complete our
+[Individual Contributor License Agreement (CLA)][cla].
+
+
+Code Contributions
+------------------
+
+Get working code on a personal branch with tests passing before you submit a PR:
+
+```
+./gradlew clean check
+```
+
+Please make every effort to follow existing conventions and style in order to keep the code as
+readable as possible.
+
+Contribute code changes through GitHub by forking the repository and sending a pull request. We
+squash all pull requests on merge.
+
+
+Committer's Guides
+------------------
+
+ * [Releasing][releasing]
+
+ [cla]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
+ [releasing]: http://square.github.io/okio/releasing/
+ [security]: http://square.github.io/okio/security/
diff --git a/LICENSE b/LICENSE
new file mode 120000
index 00000000..85de3d45
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+LICENSE.txt \ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/METADATA b/METADATA
new file mode 100644
index 00000000..659dfff2
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,20 @@
+name: "okio"
+description:
+ "Okio is a library that complements java.io and java.nio to make it much "
+ "easier to access, store, and process your data. It started as a component "
+ "of OkHttp, the capable HTTP client included in Android. It's "
+ "well-exercised and ready to solve new problems."
+
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://square.github.io/okio/"
+ }
+ url {
+ type: GIT
+ value: "https://github.com/square/okio/"
+ }
+ version: "47fb0ddcd0bcf768a897dff723a1699341eea10f"
+ last_upgrade_date { year: 2021 month: 4 day: 6 }
+ license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 00000000..25abe552
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,3 @@
+arangelov@google.com
+stanleytfwang@google.com
+alexkershaw@google.com
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..6a331ee9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+Okio
+====
+
+See the [project website][okio] for documentation and APIs.
+
+Okio is a library that complements `java.io` and `java.nio` to make it much
+easier to access, store, and process your data. It started as a component of
+[OkHttp][1], the capable HTTP client included in Android. It's well-exercised
+and ready to solve new problems.
+
+License
+--------
+
+ Copyright 2013 Square, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ [1]: https://github.com/square/okhttp
+ [okio]: https://square.github.io/okio/
diff --git a/android-test/README.md b/android-test/README.md
new file mode 100644
index 00000000..f6240051
--- /dev/null
+++ b/android-test/README.md
@@ -0,0 +1,50 @@
+Android Test
+============
+
+This module runs Okio's test suite on a connected Android emulator or device. It requires the same
+set-up as [OkHttp's android-test module][okhttp_android_test].
+
+In brief, configure the Android SDK and PATH:
+
+```
+export ANDROID_SDK_ROOT=/Users/$USER/Library/Android/sdk
+export PATH=$PATH:$ANDROID_SDK_ROOT/tools/bin:$ANDROID_SDK_ROOT/platform-tools
+```
+
+Use `logcat` to stream test logs:
+
+```
+adb logcat '*:E' TestRunner:D TaskRunner:D GnssHAL_GnssInterface:F DeviceStateChecker:F memtrack:F
+```
+
+Then run the tests:
+
+```
+./gradlew :android-test:connectedAndroidTest
+```
+
+Or just a single test:
+
+```
+./gradlew :android-test:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=okio.SystemFileSystemTest
+```
+
+
+### Watch Out For Crashing Failures
+
+Some of Okio's tests can cause the test process to crash. The test will be reported as a failure
+with a message like this:
+
+> Test failed to run to completion. Reason: 'Instrumentation run failed due to 'Process crashed.''.
+> Check device logcat for details
+
+When this happens, it's possible that tests are missing from the test run! One workaround is to
+exclude the crashing test and re-run the rest. You can confirm that the test run completed normally
+if a `run finished` line is printed in the logcat logs:
+
+```
+01-01 00:00:00.000 12345 23456 I TestRunner: run finished: 2976 tests, 0 failed, 3 ignored
+```
+
+
+[okhttp_android_test]: https://github.com/square/okhttp/tree/master/android-test
diff --git a/android-test/build.gradle b/android-test/build.gradle
new file mode 100644
index 00000000..56fcda76
--- /dev/null
+++ b/android-test/build.gradle
@@ -0,0 +1,73 @@
+apply plugin: 'com.android.library'
+apply plugin: 'org.jetbrains.kotlin.android'
+
+buildscript {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ google()
+ }
+}
+
+def isIDE = properties.containsKey('android.injected.invoked.from.ide') ||
+ (System.getenv("XPC_SERVICE_NAME") ?: "").contains("intellij") ||
+ System.getenv("IDEA_INITIAL_DIRECTORY") != null
+
+android {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ coreLibraryDesugaringEnabled true
+ }
+
+ kotlinOptions {
+ freeCompilerArgs += "-Xmulti-platform"
+ }
+
+ compileSdkVersion 30
+
+ defaultConfig {
+ minSdkVersion 15
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ // AndroidJUnitRunner wasn't finding tests in multidex artifacts when running on Android 4.0.3.
+ // Work around by adding all Okio classes to the keep list. That way they'll be in the main
+ // .dx file where TestRequestBuilder will find them.
+ multiDexEnabled true
+ multiDexKeepProguard file('multidex-config.pro')
+ }
+
+ if (!isIDE) {
+ sourceSets {
+ named("androidTest") {
+ it.java.srcDirs += [
+ project.file("../okio/src/commonMain/kotlin"),
+ project.file("../okio/src/commonTest/java"),
+ project.file("../okio/src/commonTest/kotlin"),
+ project.file("../okio/src/hashFunctions/kotlin"),
+ project.file("../okio/src/jvmMain/kotlin"),
+ project.file("../okio/src/jvmTest/java"),
+ project.file("../okio/src/jvmTest/kotlin"),
+ ]
+ }
+ }
+ }
+}
+
+
+dependencies {
+ coreLibraryDesugaring deps.android.desugarJdkLibs
+ androidTestImplementation deps.androidx.testExtJunit
+ androidTestImplementation deps.androidx.testRunner
+ androidTestImplementation deps.animalSniffer.annotations
+ androidTestImplementation deps.kotlin.stdLib.common
+ androidTestImplementation deps.kotlin.test.annotations
+ androidTestImplementation deps.kotlin.test.common
+ androidTestImplementation deps.kotlin.test.jdk
+ androidTestImplementation deps.kotlin.time
+ androidTestImplementation deps.test.assertj
+ androidTestImplementation deps.test.junit
+}
diff --git a/android-test/multidex-config.pro b/android-test/multidex-config.pro
new file mode 100644
index 00000000..ace307d3
--- /dev/null
+++ b/android-test/multidex-config.pro
@@ -0,0 +1 @@
+-keep class okio.** { *; }
diff --git a/android-test/src/main/AndroidManifest.xml b/android-test/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fe95031b
--- /dev/null
+++ b/android-test/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="MissingClass"
+ package="com.squareup.okio">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <!-- To access the system temporary directory. -->
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+ <application
+ android:name="androidx.multidex.MultiDexApplication"
+ android:usesCleartextTraffic="true" />
+
+</manifest>
diff --git a/android-test/src/main/res/values/strings.xml b/android-test/src/main/res/values/strings.xml
new file mode 100644
index 00000000..3f2b0bb2
--- /dev/null
+++ b/android-test/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">android-test</string>
+</resources>
diff --git a/android-test/src/main/res/xml/network_security_config.xml b/android-test/src/main/res/xml/network_security_config.xml
new file mode 100644
index 00000000..353fa808
--- /dev/null
+++ b/android-test/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+ <base-config cleartextTrafficPermitted="false">
+ </base-config>
+</network-security-config>
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..4923686e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,120 @@
+buildscript {
+ // If false - JS targets will not be configured in multiplatform projects.
+ ext.kmpJsEnabled = Boolean.parseBoolean(System.getProperty('kjs', 'true'))
+
+ // If false - Native targets will not be configured in multiplatform projects.
+ ext.kmpNativeEnabled = Boolean.parseBoolean(System.getProperty('knative', 'true'))
+
+ ext.versions = [
+ 'kotlin': '1.4.20',
+ 'jmhPlugin': '0.5.0',
+ 'animalSnifferPlugin': '1.5.0',
+ 'dokka': '1.4.20',
+ 'jmh': '1.23',
+ 'animalSniffer': '1.16',
+ 'junit': '4.12',
+ 'assertj': '1.7.0',
+ 'shadowPlugin': '5.2.0',
+ 'spotless': '5.8.2',
+ 'ktlint': '0.40.0',
+ 'bndPlugin': '5.1.2'
+ ]
+
+ ext.deps = [
+ 'android': [
+ 'gradlePlugin': "com.android.tools.build:gradle:4.1.1",
+ 'desugarJdkLibs': "com.android.tools:desugar_jdk_libs:1.1.1",
+ ],
+ 'androidx': [
+ 'testExtJunit': "androidx.test.ext:junit:1.1.2",
+ 'testRunner': "androidx.test:runner:1.3.0",
+ ],
+ 'kotlin': [
+ 'gradlePlugin': "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}",
+ 'stdLib': [
+ 'common': "org.jetbrains.kotlin:kotlin-stdlib-common",
+ 'jdk8': "org.jetbrains.kotlin:kotlin-stdlib-jdk8",
+ 'jdk7': "org.jetbrains.kotlin:kotlin-stdlib-jdk7",
+ 'jdk6': "org.jetbrains.kotlin:kotlin-stdlib",
+ 'js': "org.jetbrains.kotlin:kotlin-stdlib-js",
+ ],
+ 'test': [
+ 'common': "org.jetbrains.kotlin:kotlin-test-common",
+ 'annotations': "org.jetbrains.kotlin:kotlin-test-annotations-common",
+ 'jdk': "org.jetbrains.kotlin:kotlin-test-junit",
+ 'js': "org.jetbrains.kotlin:kotlin-test-js",
+ ],
+ 'time': 'org.jetbrains.kotlinx:kotlinx-datetime:0.1.1',
+ ],
+ 'jmh': [
+ 'gradlePlugin': "me.champeau.gradle:jmh-gradle-plugin:${versions.jmhPlugin}",
+ 'core': "org.openjdk.jmh:jmh-core:${versions.jmh}",
+ 'generator': "org.openjdk.jmh:jmh-generator-annprocess:${versions.jmh}",
+ ],
+ 'animalSniffer': [
+ 'gradlePlugin': "ru.vyarus:gradle-animalsniffer-plugin:${versions.animalSnifferPlugin}",
+ 'annotations': "org.codehaus.mojo:animal-sniffer-annotations:${versions.animalSniffer}",
+ ],
+ 'japicmp': 'me.champeau.gradle:japicmp-gradle-plugin:0.2.8',
+ 'dokka': "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}",
+ 'shadow': "com.github.jengelman.gradle.plugins:shadow:${versions.shadowPlugin}",
+ 'spotless': "com.diffplug.spotless:spotless-plugin-gradle:${versions.spotless}",
+ 'bnd': "biz.aQute.bnd:biz.aQute.bnd.gradle:${versions.bndPlugin}",
+ 'test': [
+ 'junit': "junit:junit:${versions.junit}",
+ 'assertj': "org.assertj:assertj-core:${versions.assertj}",
+ ]
+ ]
+
+ dependencies {
+ classpath deps.android.gradlePlugin
+ classpath deps.kotlin.gradlePlugin
+ classpath deps.animalSniffer.gradlePlugin
+ classpath deps.japicmp
+ classpath deps.dokka
+ classpath deps.shadow
+ classpath deps.jmh.gradlePlugin
+ classpath deps.spotless
+ classpath deps.bnd
+ // https://github.com/melix/japicmp-gradle-plugin/issues/36
+ classpath 'com.google.guava:guava:28.2-jre'
+ }
+
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ jcenter()
+ google()
+ maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
+ maven { url 'https://kotlin.bintray.com/kotlinx/' }
+ }
+}
+
+// when scripts are applied the buildscript classes are not accessible directly therefore we save the class here to make it accessible
+ext.bndBundleTaskConventionClass = aQute.bnd.gradle.BundleTaskConvention
+
+allprojects {
+ group = GROUP
+ version = VERSION_NAME
+}
+
+subprojects {
+ repositories {
+ mavenCentral()
+ jcenter()
+ google()
+ maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
+ maven { url 'https://kotlin.bintray.com/kotlinx/' }
+ }
+
+ apply plugin: "com.diffplug.spotless"
+
+ spotless {
+ kotlin {
+ target("**/*.kt")
+ ktlint(versions.ktlint).userData([indent_size: '2'])
+ trimTrailingWhitespace()
+ endWithNewline()
+ }
+ }
+}
diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md
new file mode 100644
index 00000000..6a97690c
--- /dev/null
+++ b/docs/code_of_conduct.md
@@ -0,0 +1,102 @@
+Open Source Code of Conduct
+===========================
+
+At Square, we are committed to contributing to the open source community and simplifying the process
+of releasing and managing open source software. We’ve seen incredible support and enthusiasm from
+thousands of people who have already contributed to our projects — and we want to ensure our community
+continues to be truly open for everyone.
+
+This code of conduct outlines our expectations for participants, as well as steps to reporting
+unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and
+expect our code of conduct to be honored.
+
+Square’s open source community strives to:
+
+ * **Be open**: We invite anyone to participate in any aspect of our projects. Our community is
+ open, and any responsibility can be carried by a contributor who demonstrates the required
+ capacity and competence.
+
+ * **Be considerate**: People use our work, and we depend on the work of others. Consider users and
+ colleagues before taking action. For example, changes to code, infrastructure, policy, and
+ documentation may negatively impact others.
+
+ * **Be respectful**: We expect people to work together to resolve conflict, assume good intentions,
+ and act with empathy. Do not turn disagreements into personal attacks.
+
+ * **Be collaborative**: Collaboration reduces redundancy and improves the quality of our work. We
+ strive for transparency within our open source community, and we work closely with upstream
+ developers and others in the free software community to coordinate our efforts.
+
+ * **Be pragmatic**: Questions are encouraged and should be asked early in the process to avoid
+ problems later. Be thoughtful and considerate when seeking out the appropriate forum for your
+ questions. Those who are asked should be responsive and helpful.
+
+ * **Step down considerately**: Members of every project come and go. When somebody leaves or
+ disengages from the project, they should make it known and take the proper steps to ensure that
+ others can pick up where they left off.
+
+This code is not exhaustive or complete. It serves to distill our common understanding of a
+collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in
+the letter.
+
+Diversity Statement
+-------------------
+
+We encourage everyone to participate and are committed to building a community for all. Although we
+may not be able to satisfy everyone, we all agree that everyone is equal.
+
+Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone
+has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do
+our best to right the wrong.
+
+Although this list cannot be exhaustive, we explicitly honor diversity in age, culture, ethnicity,
+gender identity or expression, language, national origin, political beliefs, profession, race,
+religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate
+discrimination based on any of the protected characteristics above, including participants with
+disabilities.
+
+Reporting Issues
+----------------
+
+If you experience or witness unacceptable behavior — or have any other concerns — please report it by
+emailing [codeofconduct@squareup.com][codeofconduct_at]. For more details, please see our Reporting
+Guidelines below.
+
+Thanks
+------
+
+Some of the ideas and wording for the statements and guidelines above were based on work by the
+[Twitter][twitter_coc], [Ubuntu][ubuntu_coc], [GDC][gdc_coc], and [Django][django_coc] communities.
+We are thankful for their work.
+
+Reporting Guide
+---------------
+
+If you experience or witness unacceptable behavior — or have any other concerns — please report it by
+emailing [codeofconduct@squareup.com][codeofconduct_at]. All reports will be handled with
+discretion.
+
+In your report please include:
+
+ * Your contact information.
+ * Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional
+ witnesses, please include them as well.
+ * Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly
+ available record (e.g. a mailing list archive or a public IRC logger), please include a link.
+ * Any additional information that may be helpful.
+
+After filing a report, a representative from the Square Code of Conduct committee will contact you
+personally. The committee will then review the incident, follow up with any additional questions,
+and make a decision as to how to respond.
+
+Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual
+engages in unacceptable behavior, the Square Code of Conduct committee may take any action they deem
+appropriate, up to and including a permanent ban from all of Square spaces without warning.
+
+
+[codeofconduct_at]: mailto:codeofconduct@squareup.com
+[twitter_coc]: https://github.com/twitter/code-of-conduct/blob/master/code-of-conduct.md
+[ubuntu_coc]: https://ubuntu.com/community/code-of-conduct
+[gdc_coc]: https://www.gdconf.com/code-of-conduct
+[django_coc]: https://www.djangoproject.com/conduct/reporting/
+
diff --git a/docs/css/app.css b/docs/css/app.css
new file mode 100644
index 00000000..48136b7e
--- /dev/null
+++ b/docs/css/app.css
@@ -0,0 +1,48 @@
+@font-face {
+ font-family: cash-market;
+ src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Regular.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal
+}
+
+@font-face {
+ font-family: cash-market;
+ src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Medium.woff2") format("woff2");
+ font-weight: 500;
+ font-style: normal
+}
+
+@font-face {
+ font-family: cash-market;
+ src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Bold.woff2") format("woff2");
+ font-weight: 700;
+ font-style: normal
+}
+
+body, input {
+ font-family: cash-market,"Helvetica Neue",helvetica,sans-serif;
+}
+
+.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4 {
+ font-family: cash-market,"Helvetica Neue",helvetica,sans-serif;
+ line-height: normal;
+ font-weight: bold;
+ color: #353535;
+}
+
+button.dl {
+ font-weight: 300;
+ font-size: 25px;
+ line-height: 40px;
+ padding: 3px 10px;
+ display: inline-block;
+ border-radius: 6px;
+ color: #f0f0f0;
+ margin: 5px 0;
+ width: auto;
+}
+
+.logo {
+ text-align: center;
+ margin-top: 150px;
+}
diff --git a/docs/css/dokka-logo.css b/docs/css/dokka-logo.css
new file mode 100644
index 00000000..ae3a99e6
--- /dev/null
+++ b/docs/css/dokka-logo.css
@@ -0,0 +1,6 @@
+#logo {
+ background-image: url(../images/logo-square.png);
+ background-size: auto;
+ padding-top: unset;
+ height: 60px;
+}
diff --git a/docs/images/icon-square.png b/docs/images/icon-square.png
new file mode 100644
index 00000000..bdc98d1c
--- /dev/null
+++ b/docs/images/icon-square.png
Binary files differ
diff --git a/docs/images/logo-square.png b/docs/images/logo-square.png
new file mode 100644
index 00000000..788b301a
--- /dev/null
+++ b/docs/images/logo-square.png
Binary files differ
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..d69c409d
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,1163 @@
+Okio
+====
+
+Okio is a library that complements `java.io` and `java.nio` to make it much
+easier to access, store, and process your data. It started as a component of
+[OkHttp][1], the capable HTTP client included in Android. It's well-exercised
+and ready to solve new problems.
+
+ByteStrings and Buffers
+-----------------------
+
+Okio is built around two types that pack a lot of capability into a
+straightforward API:
+
+ * [**ByteString**][3] is an immutable sequence of bytes. For character data, `String`
+ is fundamental. `ByteString` is String's long-lost brother, making it easy to
+ treat binary data as a value. This class is ergonomic: it knows how to encode
+ and decode itself as hex, base64, and UTF-8.
+
+ * [**Buffer**][4] is a mutable sequence of bytes. Like `ArrayList`, you don't need
+ to size your buffer in advance. You read and write buffers as a queue: write
+ data to the end and read it from the front. There's no obligation to manage
+ positions, limits, or capacities.
+
+Internally, `ByteString` and `Buffer` do some clever things to save CPU and
+memory. If you encode a UTF-8 string as a `ByteString`, it caches a reference to
+that string so that if you decode it later, there's no work to do.
+
+`Buffer` is implemented as a linked list of segments. When you move data from
+one buffer to another, it _reassigns ownership_ of the segments rather than
+copying the data across. This approach is particularly helpful for multithreaded
+programs: a thread that talks to the network can exchange data with a worker
+thread without any copying or ceremony.
+
+Sources and Sinks
+-----------------
+
+An elegant part of the `java.io` design is how streams can be layered for
+transformations like encryption and compression. Okio includes its own stream
+types called [`Source`][5] and [`Sink`][6] that work like `InputStream` and
+`OutputStream`, but with some key differences:
+
+ * **Timeouts.** The streams provide access to the timeouts of the underlying
+ I/O mechanism. Unlike the `java.io` socket streams, both `read()` and
+ `write()` calls honor timeouts.
+
+ * **Easy to implement.** `Source` declares three methods: `read()`, `close()`,
+ and `timeout()`. There are no hazards like `available()` or single-byte reads
+ that cause correctness and performance surprises.
+
+ * **Easy to use.** Although _implementations_ of `Source` and `Sink` have only
+ three methods to write, _callers_ are given a rich API with the
+ [`BufferedSource`][7] and [`BufferedSink`][8] interfaces. These interfaces give you
+ everything you need in one place.
+
+ * **No artificial distinction between byte streams and char streams.** It's all
+ data. Read and write it as bytes, UTF-8 strings, big-endian 32-bit integers,
+ little-endian shorts; whatever you want. No more `InputStreamReader`!
+
+ * **Easy to test.** The `Buffer` class implements both `BufferedSource` and
+ `BufferedSink` so your test code is simple and clear.
+
+Sources and sinks interoperate with `InputStream` and `OutputStream`. You can
+view any `Source` as an `InputStream`, and you can view any `InputStream` as a
+`Source`. Similarly for `Sink` and `OutputStream`.
+
+
+Presentations
+-------------
+
+[A Few “Ok” Libraries][ok_libraries_talk] ([slides][ok_libraries_slides]): An introduction to Okio
+and three libraries written with it.
+
+[Decoding the Secrets of Binary Data][encoding_talk] ([slides][encoding_slides]): How data encoding
+works and how Okio does it.
+
+[Ok Multiplatform!][ok_multiplatform_talk] ([slides][ok_multiplatform_slides]): How we changed
+Okio’s implementation language from Java to Kotlin.
+
+
+Requirements
+------------
+
+Okio supports Android 4.0.3+ (API level 15+) and Java 7+.
+
+Okio depends on the [Kotlin standard library][kotlin]. It is a small library with strong
+backward-compatibility.
+
+
+Recipes
+-------
+
+We've written some recipes that demonstrate how to solve common problems with
+Okio. Read through them to learn about how everything works together.
+Cut-and-paste these examples freely; that's what they're for.
+
+### Read a text file line-by-line ([Java][ReadFileLineByLine]/[Kotlin][ReadFileLineByLineKt])
+
+Use `Okio.source(File)` to open a source stream to read a file. The returned
+`Source` interface is very small and has limited uses. Instead we wrap the
+source with a buffer. This has two benefits:
+
+ * **It makes the API more powerful.** Instead of the basic methods offered by
+ `Source`, `BufferedSource` has dozens of methods to address most common
+ problems concisely.
+
+ * **It makes your program run faster.** Buffering allows Okio to get more done
+ with fewer I/O operations.
+
+Each `Source` that is opened needs to be closed. The code that opens the stream
+is responsible for making sure it is closed.
+
+=== "Java"
+
+ Here we use Java's `try` blocks to close our sources automatically.
+
+ ```java
+ public void readLines(File file) throws IOException {
+ try (Source fileSource = Okio.source(file);
+ BufferedSource bufferedSource = Okio.buffer(fileSource)) {
+
+ while (true) {
+ String line = bufferedSource.readUtf8Line();
+ if (line == null) break;
+
+ if (line.contains("square")) {
+ System.out.println(line);
+ }
+ }
+
+ }
+ }
+ ```
+
+=== "Kotlin"
+
+ Note that static `Okio` methods become extension functions (`Okio.source(file)` =>
+ `file.source()`), and `use` is used to automatically close the streams:
+
+ ```kotlin
+ @Throws(IOException::class)
+ fun readLines(file: File) {
+ file.source().use { fileSource ->
+ fileSource.buffer().use { bufferedFileSource ->
+ while (true) {
+ val line = bufferedFileSource.readUtf8Line() ?: break
+ if ("square" in line) {
+ println(line)
+ }
+ }
+ }
+ }
+ }
+ ```
+
+The `readUtf8Line()` API reads all of the data until the next line delimiter –
+either `\n`, `\r\n`, or the end of the file. It returns that data as a string,
+omitting the delimiter at the end. When it encounters empty lines the method
+will return an empty string. If there isn’t any more data to read it will
+return null.
+
+The above program can be written more compactly by inlining the `fileSource`
+variable and by using a fancy `for` loop instead of a `while`:
+
+```java
+public void readLines(File file) throws IOException {
+ try (BufferedSource source = Okio.buffer(Okio.source(file))) {
+ for (String line; (line = source.readUtf8Line()) != null; ) {
+ if (line.contains("square")) {
+ System.out.println(line);
+ }
+ }
+ }
+}
+```
+
+In Kotlin, we can wrap invocations of `source.readUtf8Line()` into the `generateSequence` builder to
+create a sequence of lines that will end once null is returned. Plus, transforming streams is easy
+thanks to the extension functions:
+
+```kotlin
+@Throws(IOException::class)
+fun readLines(file: File) {
+ file.source().buffer().use { source ->
+ generateSequence { source.readUtf8Line() }
+ .filter { line -> "square" in line }
+ .forEach(::println)
+ }
+}
+```
+
+The `readUtf8Line()` method is suitable for parsing most files. For certain
+use-cases you may also consider `readUtf8LineStrict()`. It is similar but it
+requires that each line is terminated by `\n` or `\r\n`. If it encounters the
+end of the file before that it will throw an `EOFException`. The strict variant
+also permits a byte limit to defend against malformed input.
+
+```java
+public void readLines(File file) throws IOException {
+ try (BufferedSource source = Okio.buffer(Okio.source(file))) {
+ while (!source.exhausted()) {
+ String line = source.readUtf8LineStrict(1024L);
+ if (line.contains("square")) {
+ System.out.println(line);
+ }
+ }
+ }
+}
+```
+
+Here's a similar example written in Kotlin:
+
+```kotlin
+@Throws(IOException::class)
+fun readLines(file: File) {
+ file.source().buffer().use { source ->
+ while (!source.exhausted()) {
+ val line = source.readUtf8LineStrict(1024)
+ if ("square" in line) {
+ println(line)
+ }
+ }
+ }
+}
+```
+
+### Write a text file ([Java][WriteFile]/[Kotlin][WriteFileKt])
+
+Above we used a `Source` and a `BufferedSource` to read a file. To write, we use
+a `Sink` and a `BufferedSink`. The advantages of buffering are the same: a more
+capable API and better performance.
+
+```java
+public void writeEnv(File file) throws IOException {
+ try (Sink fileSink = Okio.sink(file);
+ BufferedSink bufferedSink = Okio.buffer(fileSink)) {
+
+ for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
+ bufferedSink.writeUtf8(entry.getKey());
+ bufferedSink.writeUtf8("=");
+ bufferedSink.writeUtf8(entry.getValue());
+ bufferedSink.writeUtf8("\n");
+ }
+
+ }
+}
+```
+
+There isn’t an API to write a line of input; instead we manually insert our own
+newline character. Most programs should hardcode `"\n"` as the newline
+character. In rare situations you may use `System.lineSeparator()` instead of
+`"\n"`: it returns `"\r\n"` on Windows and `"\n"` everywhere else.
+
+We can write the above program more compactly by inlining the `fileSink`
+variable and by taking advantage of method chaining:
+
+=== "Java"
+
+ ```Java
+ public void writeEnv(File file) throws IOException {
+ try (BufferedSink sink = Okio.buffer(Okio.sink(file))) {
+ for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
+ sink.writeUtf8(entry.getKey())
+ .writeUtf8("=")
+ .writeUtf8(entry.getValue())
+ .writeUtf8("\n");
+ }
+ }
+ }
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ @Throws(IOException::class)
+ fun writeEnv(file: File) {
+ file.sink().buffer().use { sink ->
+ for ((key, value) in System.getenv()) {
+ sink.writeUtf8(key)
+ sink.writeUtf8("=")
+ sink.writeUtf8(value)
+ sink.writeUtf8("\n")
+ }
+ }
+ }
+ ```
+
+In the above code we make four calls to `writeUtf8()`. Making four calls is
+more efficient than the code below because the VM doesn’t have to create and
+garbage collect a temporary string.
+
+```java
+sink.writeUtf8(entry.getKey() + "=" + entry.getValue() + "\n"); // Slower!
+```
+
+### UTF-8 ([Java][ExploreCharsets]/[Kotlin][ExploreCharsetsKt])
+
+In the above APIs you can see that Okio really likes UTF-8. Early computer
+systems suffered many incompatible character encodings: ISO-8859-1, ShiftJIS,
+ASCII, EBCDIC, etc. Writing software to support multiple character sets was
+awful and we didn’t even have emoji! Today we're lucky that the world has
+standardized on UTF-8 everywhere, with some rare uses of other charsets in
+legacy systems.
+
+If you need another character set, `readString()` and `writeString()` are there
+for you. These methods require that you specify a character set. Otherwise you
+may accidentally create data that is only readable by the local computer. Most
+programs should use the UTF-8 methods only.
+
+When encoding strings you need to be mindful of the different ways that strings
+are represented and encoded. When a glyph has an accent or another adornment
+it may be represented as a single complex code point (`é`) or as a simple code
+point (`e`) followed by its modifiers (`´`). When the entire glyph is a single
+code point that’s called [NFC][nfc]; when it’s multiple it’s [NFD][nfd].
+
+Though we use UTF-8 whenever we read or write strings in I/O, when they are in
+memory Java Strings use an obsolete character encoding called UTF-16. It is a
+bad encoding because it uses a 16-bit `char` for most characters, but some don’t
+fit. In particular, most emoji use two Java chars. This is problematic because
+`String.length()` returns a surprising result: the number of UTF-16 chars and
+not the natural number of glyphs.
+
+| | Café 🍩 | Café 🍩 |
+| --------------------: | :---------------------------| :------------------------------|
+| Form | [NFC][nfc] | [NFD][nfd] |
+| Code Points | `c  a  f  é    ␣   🍩     ` | `c  a  f  e  ´    ␣   🍩     ` |
+| UTF-8 bytes | `43 61 66 c3a9 20 f09f8da9` | `43 61 66 65 cc81 20 f09f8da9` |
+| String.codePointCount | 6 | 7 |
+| String.length | 7 | 8 |
+| Utf8.size | 10 | 11 |
+
+For the most part Okio lets you ignore these problems and focus on your data.
+But when you need them, there are convenient APIs for dealing with low-level
+UTF-8 strings.
+
+Use `Utf8.size()` to count the number of bytes required to encode a string as
+UTF-8 without actually encoding it. This is handy in length-prefixed encodings
+like protocol buffers.
+
+Use `BufferedSource.readUtf8CodePoint()` to read a single variable-length code
+point, and `BufferedSink.writeUtf8CodePoint()` to write one.
+
+### Golden Values ([Java][GoldenValue]/[Kotlin][GoldenValueKt])
+
+Okio likes testing. The library itself is heavily tested, and it has features
+that are often helpful when testing application code. One pattern we’ve found to
+be quite useful is “golden value” testing. The goal of such tests is to confirm
+that data encoded with earlier versions of a program can safely be decoded by
+the current program.
+
+We’ll illustrate this by encoding a value using Java Serialization. Though we
+must disclaim that Java Serialization is an awful encoding system and most
+programs should prefer other formats like JSON or protobuf! In any case, here’s
+a method that takes an object, serializes it, and returns the result as a
+`ByteString`:
+
+=== "Java"
+
+ ```Java
+ private ByteString serialize(Object o) throws IOException {
+ Buffer buffer = new Buffer();
+ try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) {
+ objectOut.writeObject(o);
+ }
+ return buffer.readByteString();
+ }
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ @Throws(IOException::class)
+ private fun serialize(o: Any?): ByteString {
+ val buffer = Buffer()
+ ObjectOutputStream(buffer.outputStream()).use { objectOut ->
+ objectOut.writeObject(o)
+ }
+ return buffer.readByteString()
+ }
+ ```
+
+There’s a lot going on here.
+
+1. We create a buffer as a holding space for our serialized data. It’s a convenient
+ replacement for `ByteArrayOutputStream`.
+
+2. We ask the buffer for its output stream. Writes to a buffer or its output stream
+ always append data to the end of the buffer.
+
+3. We create an `ObjectOutputStream` (the encoding API for Java serialization) and
+ write our object. The try block takes care of closing the stream for us. Note
+ that closing a buffer has no effect.
+
+4. Finally we read a byte string from the buffer. The `readByteString()` method
+ allows us to specify how many bytes to read; here we don’t specify a count in
+ order to read the entire thing. Reads from a buffer always consume data from
+ the front of the buffer.
+
+With our `serialize()` method handy we are ready to compute and print a golden
+value.
+
+=== "Java"
+
+ ```Java
+ Point point = new Point(8.0, 15.0);
+ ByteString pointBytes = serialize(point);
+ System.out.println(pointBytes.base64());
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ val point = Point(8.0, 15.0)
+ val pointBytes = serialize(point)
+ println(pointBytes.base64())
+ ```
+
+We print the `ByteString` as [base64][base64] because it’s a compact format
+that’s suitable for embedding in a test case. The program prints this:
+
+```
+rO0ABXNyAB5va2lvLnNhbXBsZXMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA
+```
+
+That’s our golden value! We can embed it in our test case using base64 again
+to convert it back into a `ByteString`:
+
+=== "Java"
+
+ ```Java
+ ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ"
+ + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA"
+ + "AAAAAAA");
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" +
+ "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64()
+ ```
+
+The next step is to deserialize the `ByteString` back into our value class. This
+method reverses the `serialize()` method above: we append a byte string to a
+buffer then consume it using an `ObjectInputStream`:
+
+=== "Java"
+
+ ```Java
+ private Object deserialize(ByteString byteString) throws IOException, ClassNotFoundException {
+ Buffer buffer = new Buffer();
+ buffer.write(byteString);
+ try (ObjectInputStream objectIn = new ObjectInputStream(buffer.inputStream())) {
+ return objectIn.readObject();
+ }
+ }
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ @Throws(IOException::class, ClassNotFoundException::class)
+ private fun deserialize(byteString: ByteString): Any? {
+ val buffer = Buffer()
+ buffer.write(byteString)
+ ObjectInputStream(buffer.inputStream()).use { objectIn ->
+ return objectIn.readObject()
+ }
+ }
+ ```
+
+Now we can test the decoder against the golden value:
+
+=== "Java"
+
+ ```Java
+ ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ"
+ + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA"
+ + "AAAAAAA");
+ Point decoded = (Point) deserialize(goldenBytes);
+ assertEquals(new Point(8.0, 15.0), decoded);
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" +
+ "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64()!!
+ val decoded = deserialize(goldenBytes) as Point
+ assertEquals(point, decoded)
+ ```
+
+With this test we can change the serialization of the `Point` class without
+breaking compatibility.
+
+
+### Write a binary file ([Java][BitmapEncoder]/[Kotlin][BitmapEncoderKt])
+
+Encoding a binary file is not unlike encoding a text file. Okio uses the same
+`BufferedSink` and `BufferedSource` bytes for both. This is handy for binary
+formats that include both byte and character data.
+
+Writing binary data is more hazardous than text because if you make a mistake it
+is often quite difficult to diagnose. Avoid such mistakes by being careful
+around these traps:
+
+ * **The width of each field.** This is the number of bytes used. Okio doesn't
+ include a mechanism to emit partial bytes. If you need that, you’ll need to
+ do your own bit shifting and masking before writing.
+
+ * **The endianness of each field.** All fields that have more than one byte
+ have _endianness_: whether the bytes are ordered most-significant to least
+ (big endian) or least-significant to most (little endian). Okio uses the `Le`
+ suffix for little-endian methods; methods without a suffix are big-endian.
+
+ * **Signed vs. Unsigned.** Java doesn’t have unsigned primitive types (except
+ for `char`!) so coping with this is often something that happens at the
+ application layer. To make this a little easier Okio accepts `int` types for
+ `writeByte()` and `writeShort()`. You can pass an “unsigned” byte like 255
+ and Okio will do the right thing.
+
+| Method | Width | Endianness | Value | Encoded Value |
+| :----------- | ----: | :--------- | --------------: | :------------------------ |
+| writeByte | 1 | | 3 | `03` |
+| writeShort | 2 | big | 3 | `00 03` |
+| writeInt | 4 | big | 3 | `00 00 00 03` |
+| writeLong | 8 | big | 3 | `00 00 00 00 00 00 00 03` |
+| writeShortLe | 2 | little | 3 | `03 00` |
+| writeIntLe | 4 | little | 3 | `03 00 00 00` |
+| writeLongLe | 8 | little | 3 | `03 00 00 00 00 00 00 00` |
+| writeByte | 1 | | Byte.MAX_VALUE | `7f` |
+| writeShort | 2 | big | Short.MAX_VALUE | `7f ff` |
+| writeInt | 4 | big | Int.MAX_VALUE | `7f ff ff ff` |
+| writeLong | 8 | big | Long.MAX_VALUE | `7f ff ff ff ff ff ff ff` |
+| writeShortLe | 2 | little | Short.MAX_VALUE | `ff 7f` |
+| writeIntLe | 4 | little | Int.MAX_VALUE | `ff ff ff 7f` |
+| writeLongLe | 8 | little | Long.MAX_VALUE | `ff ff ff ff ff ff ff 7f` |
+
+This code encodes a bitmap following the [BMP file format][bmp].
+
+=== "Java"
+
+ ```Java
+ void encode(Bitmap bitmap, BufferedSink sink) throws IOException {
+ int height = bitmap.height();
+ int width = bitmap.width();
+
+ int bytesPerPixel = 3;
+ int rowByteCountWithoutPadding = (bytesPerPixel * width);
+ int rowByteCount = ((rowByteCountWithoutPadding + 3) / 4) * 4;
+ int pixelDataSize = rowByteCount * height;
+ int bmpHeaderSize = 14;
+ int dibHeaderSize = 40;
+
+ // BMP Header
+ sink.writeUtf8("BM"); // ID.
+ sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize); // File size.
+ sink.writeShortLe(0); // Unused.
+ sink.writeShortLe(0); // Unused.
+ sink.writeIntLe(bmpHeaderSize + dibHeaderSize); // Offset of pixel data.
+
+ // DIB Header
+ sink.writeIntLe(dibHeaderSize);
+ sink.writeIntLe(width);
+ sink.writeIntLe(height);
+ sink.writeShortLe(1); // Color plane count.
+ sink.writeShortLe(bytesPerPixel * Byte.SIZE);
+ sink.writeIntLe(0); // No compression.
+ sink.writeIntLe(16); // Size of bitmap data including padding.
+ sink.writeIntLe(2835); // Horizontal print resolution in pixels/meter. (72 dpi).
+ sink.writeIntLe(2835); // Vertical print resolution in pixels/meter. (72 dpi).
+ sink.writeIntLe(0); // Palette color count.
+ sink.writeIntLe(0); // 0 important colors.
+
+ // Pixel data.
+ for (int y = height - 1; y >= 0; y--) {
+ for (int x = 0; x < width; x++) {
+ sink.writeByte(bitmap.blue(x, y));
+ sink.writeByte(bitmap.green(x, y));
+ sink.writeByte(bitmap.red(x, y));
+ }
+
+ // Padding for 4-byte alignment.
+ for (int p = rowByteCountWithoutPadding; p < rowByteCount; p++) {
+ sink.writeByte(0);
+ }
+ }
+ }
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ @Throws(IOException::class)
+ fun encode(bitmap: Bitmap, sink: BufferedSink) {
+ val height = bitmap.height
+ val width = bitmap.width
+ val bytesPerPixel = 3
+ val rowByteCountWithoutPadding = bytesPerPixel * width
+ val rowByteCount = (rowByteCountWithoutPadding + 3) / 4 * 4
+ val pixelDataSize = rowByteCount * height
+ val bmpHeaderSize = 14
+ val dibHeaderSize = 40
+
+ // BMP Header
+ sink.writeUtf8("BM") // ID.
+ sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize) // File size.
+ sink.writeShortLe(0) // Unused.
+ sink.writeShortLe(0) // Unused.
+ sink.writeIntLe(bmpHeaderSize + dibHeaderSize) // Offset of pixel data.
+
+ // DIB Header
+ sink.writeIntLe(dibHeaderSize)
+ sink.writeIntLe(width)
+ sink.writeIntLe(height)
+ sink.writeShortLe(1) // Color plane count.
+ sink.writeShortLe(bytesPerPixel * Byte.SIZE_BITS)
+ sink.writeIntLe(0) // No compression.
+ sink.writeIntLe(16) // Size of bitmap data including padding.
+ sink.writeIntLe(2835) // Horizontal print resolution in pixels/meter. (72 dpi).
+ sink.writeIntLe(2835) // Vertical print resolution in pixels/meter. (72 dpi).
+ sink.writeIntLe(0) // Palette color count.
+ sink.writeIntLe(0) // 0 important colors.
+
+ // Pixel data.
+ for (y in height - 1 downTo 0) {
+ for (x in 0 until width) {
+ sink.writeByte(bitmap.blue(x, y))
+ sink.writeByte(bitmap.green(x, y))
+ sink.writeByte(bitmap.red(x, y))
+ }
+
+ // Padding for 4-byte alignment.
+ for (p in rowByteCountWithoutPadding until rowByteCount) {
+ sink.writeByte(0)
+ }
+ }
+ }
+ ```
+
+The trickiest part of this program is the format’s required padding. The BMP
+format expects each row to begin on a 4-byte boundary so it is necessary to add
+zeros to maintain the alignment.
+
+Encoding other binary formats is usually quite similar. Some tips:
+
+ * Write tests with golden values! Confirming that your program emits the
+ expected result can make debugging easier.
+ * Use `Utf8.size()` to compute the number of bytes of an encoded string. This
+ is essential for length-prefixed formats.
+ * Use `Float.floatToIntBits()` and `Double.doubleToLongBits()` to encode
+ floating point values.
+
+
+### Communicate on a Socket ([Java][SocksProxyServer]/[Kotlin][SocksProxyServerKt])
+
+Sending and receiving data over the network is a bit like writing and reading
+files. We use `BufferedSink` to encode output and `BufferedSource` to decode
+input. Like files, network protocols can be text, binary, or a mix of both. But
+there are also some substantial differences between the network and the
+file system.
+
+With a file you’re either reading or writing but with the network you can do
+both! Some protocols handle this by taking turns: write a request, read a
+response, repeat. You can implement this kind of protocol with a single thread.
+In other protocols you may read and write simultaneously. Typically you’ll want
+one dedicated thread for reading. For writing you can use either a dedicated
+thread or use `synchronized` so that multiple threads can share a sink. Okio’s
+streams are not safe for concurrent use.
+
+Sinks buffer outbound data to minimize I/O operations. This is efficient but it
+means you must manually call `flush()` to transmit data. Typically
+message-oriented protocols flush after each message. Note that Okio will
+automatically flush when the buffered data exceeds some threshold. This is
+intended to save memory and you shouldn’t rely on it for interactive protocols.
+
+Okio builds on `java.io.Socket` for connectivity. Create your socket as a server
+or as a client, then use `Okio.source(Socket)` to read and `Okio.sink(Socket)`
+to write. These APIs also work with `SSLSocket`. You should use SSL unless you
+have a very good reason not to!
+
+Cancel a socket from any thread by calling `Socket.close()`; this will cause its
+sources and sinks to immediately fail with an `IOException`. You can also
+configure timeouts for all socket operations. You don’t need a reference to the
+socket to adjust timeouts: `Source` and `Sink` expose timeouts directly. This
+API works even if the streams are decorated.
+
+As a complete example of networking with Okio we wrote a [basic SOCKS
+proxy][SocksProxyServer] server. Some highlights:
+
+=== "Java"
+
+ ```Java
+ Socket fromSocket = ...
+ BufferedSource fromSource = Okio.buffer(Okio.source(fromSocket));
+ BufferedSink fromSink = Okio.buffer(Okio.sink(fromSocket));
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ val fromSocket: Socket = ...
+ val fromSource = fromSocket.source().buffer()
+ val fromSink = fromSocket.sink().buffer()
+ ```
+
+Creating sources and sinks for sockets is the same as creating them for files.
+Once you create a `Source` or `Sink` for a socket you must not use its
+`InputStream` or `OutputStream`, respectively.
+
+=== "Java"
+
+ ```Java
+ Buffer buffer = new Buffer();
+ for (long byteCount; (byteCount = source.read(buffer, 8192L)) != -1; ) {
+ sink.write(buffer, byteCount);
+ sink.flush();
+ }
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ val buffer = Buffer()
+ var byteCount: Long
+ while (source.read(buffer, 8192L).also { byteCount = it } != -1L) {
+ sink.write(buffer, byteCount)
+ sink.flush()
+ }
+ ```
+
+The above loop copies data from the source to the sink, flushing after each
+read. If we didn’t need the flushing we could replace this loop with a single
+call to `BufferedSink.writeAll(Source)`.
+
+The `8192` argument to `read()` is the maximum number of bytes to read before
+returning. We could have passed any value here, but we like 8 KiB because that’s
+the largest value Okio can do in a single system call. Most of the time
+application code doesn’t need to deal with such limits!
+
+=== "Java"
+
+ ```Java
+ int addressType = fromSource.readByte() & 0xff;
+ int port = fromSource.readShort() & 0xffff;
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ val addressType = fromSource.readByte().toInt() and 0xff
+ val port = fromSource.readShort().toInt() and 0xffff
+ ```
+
+Okio uses signed types like `byte` and `short`, but often protocols want
+unsigned values. The bitwise `&` operator is Java’s preferred idiom to convert
+a signed value into an unsigned value. Here’s a cheat sheet for bytes, shorts,
+and ints:
+
+| Type | Signed Range | Unsigned Range | Signed to Unsigned |
+| :---- | :---------------------------: | :--------------- | :-------------------------- |
+| byte | -128..127 | 0..255 | `int u = s & 0xff;` |
+| short | -32,768..32,767 | 0..65,535 | `int u = s & 0xffff;` |
+| int | -2,147,483,648..2,147,483,647 | 0..4,294,967,295 | `long u = s & 0xffffffffL;` |
+
+Java has no primitive type that can represent unsigned longs.
+
+
+### Hashing ([Java][Hashing]/[Kotlin][HashingKt])
+
+We’re bombarded by hashing in our lives as Java programmers. Early on we're introduced to the
+`hashCode()` method, something we know we need to override otherwise unforeseen bad things happen.
+Later we’re shown `LinkedHashMap` and its friends. These build on that `hashCode()` method to
+organize data for fast retrieval.
+
+Elsewhere we have cryptographic hash functions. These get used all over the place. HTTPS
+certificates, Git commits, BitTorrent integrity checking, and Blockchain blocks all use
+cryptographic hashes. Good use of hashes can improve the performance, privacy, security, and
+simplicity of an application.
+
+Each cryptographic hash function accepts a variable-length stream of input bytes and produces a
+fixed-length byte string value called the “hash”. Hash functions have these important qualities:
+
+ * Deterministic: each input always produces the same output.
+ * Uniform: each output byte string is equally likely. It is very difficult to find or create pairs
+ of different inputs that yield the same output. This is called a “collision”.
+ * Non-reversible: knowing an output doesn't help you to find the input. Note that if you know some
+ possible inputs you can hash them to see if their hashes match.
+ * Well-known: the hash is implemented everywhere and rigorously understood.
+
+Good hash functions are very cheap to compute (dozens of microseconds) and expensive to reverse
+(quintillions of millenia). Steady advances in computing and mathematics have caused once-great hash
+functions to become inexpensive to reverse. When choosing a hash function, beware that not all are
+created equal! Okio supports these well-known cryptographic hash functions:
+
+ * **MD5**: a 128-bit (16 byte) cryptographic hash. It is both insecure and obsolete because it is
+ inexpensive to reverse! This hash is offered because it is popular and convenient for use in
+ legacy systems that are not security-sensitive.
+ * **SHA-1**: a 160-bit (20 byte) cryptographic hash. It was recently demonstrated that it is
+ feasible to create SHA-1 collisions. Consider upgrading from SHA-1 to SHA-256.
+ * **SHA-256**: a 256-bit (32 byte) cryptographic hash. SHA-256 is widely understood and expensive
+ to reverse. This is the hash most systems should use.
+ * **SHA-512**: a 512-bit (64 byte) cryptographic hash. It is expensive to reverse.
+
+Each hash creates a `ByteString` of the specified length. Use `hex()` to get the conventional
+human-readable form. Or leave it as a `ByteString` because that’s a convenient model type!
+
+Okio can produce cryptographic hashes from byte strings:
+
+=== "Java"
+
+ ```Java
+ ByteString byteString = readByteString(new File("README.md"));
+ System.out.println(" md5: " + byteString.md5().hex());
+ System.out.println(" sha1: " + byteString.sha1().hex());
+ System.out.println("sha256: " + byteString.sha256().hex());
+ System.out.println("sha512: " + byteString.sha512().hex());
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ val byteString = readByteString(File("README.md"))
+ println(" md5: " + byteString.md5().hex())
+ println(" sha1: " + byteString.sha1().hex())
+ println(" sha256: " + byteString.sha256().hex())
+ println(" sha512: " + byteString.sha512().hex())
+ ```
+
+From buffers:
+
+=== "Java"
+
+ ```Java
+ Buffer buffer = readBuffer(new File("README.md"));
+ System.out.println(" md5: " + buffer.md5().hex());
+ System.out.println(" sha1: " + buffer.sha1().hex());
+ System.out.println("sha256: " + buffer.sha256().hex());
+ System.out.println("sha512: " + buffer.sha512().hex());
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ val buffer = readBuffer(File("README.md"))
+ println(" md5: " + buffer.md5().hex())
+ println(" sha1: " + buffer.sha1().hex())
+ println(" sha256: " + buffer.sha256().hex())
+ println(" sha512: " + buffer.sha512().hex())
+ ```
+
+While streaming from a source:
+
+=== "Java"
+
+ ```Java
+ try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());
+ BufferedSource source = Okio.buffer(Okio.source(file))) {
+ source.readAll(hashingSink);
+ System.out.println("sha256: " + hashingSink.hash().hex());
+ }
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ sha256(blackholeSink()).use { hashingSink ->
+ file.source().buffer().use { source ->
+ source.readAll(hashingSink)
+ println(" sha256: " + hashingSink.hash.hex())
+ }
+ }
+ ```
+
+While streaming to a sink:
+
+=== "Java"
+
+ ```Java
+ try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());
+ BufferedSink sink = Okio.buffer(hashingSink);
+ Source source = Okio.source(file)) {
+ sink.writeAll(source);
+ sink.close(); // Emit anything buffered.
+ System.out.println("sha256: " + hashingSink.hash().hex());
+ }
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ sha256(blackholeSink()).use { hashingSink ->
+ hashingSink.buffer().use { sink ->
+ file.source().use { source ->
+ sink.writeAll(source)
+ sink.close() // Emit anything buffered.
+ println(" sha256: " + hashingSink.hash.hex())
+ }
+ }
+ }
+ ```
+
+Okio also supports HMAC (Hash Message Authentication Code) which combines a secret and a hash.
+Applications use HMAC for data integrity and authentication.
+
+=== "Java"
+
+ ```Java
+ ByteString secret = ByteString.decodeHex("7065616e7574627574746572");
+ System.out.println("hmacSha256: " + byteString.hmacSha256(secret).hex());
+ ```
+
+=== "Kotlin"
+
+ ```Kotlin
+ val secret = "7065616e7574627574746572".decodeHex()
+ println("hmacSha256: " + byteString.hmacSha256(secret).hex())
+ ```
+
+As with hashing, you can generate an HMAC from a `ByteString`, `Buffer`, `HashingSource`, and
+`HashingSink`. Note that Okio doesn’t implement HMAC for MD5. Okio uses Java’s
+`java.security.MessageDigest` for cryptographic hashes and `javax.crypto.Mac` for HMAC.
+
+### Encryption and Decryption
+
+Use `Okio.cipherSink(Sink, Cipher)` or `Okio.cipherSource(Source, Cipher)` to encrypt or decrypt a
+stream using a block cipher.
+
+Callers are responsible for the initialization of the encryption or decryption cipher with the
+chosen algorithm, the key, and algorithm-specific additional parameters like the initialization
+vector. The following example shows a typical usage with AES encryption, in which `key` and `iv`
+parameters should both be 16 bytes long.
+
+```java
+void encryptAes(ByteString bytes, File file, byte[] key, byte[] iv)
+ throws GeneralSecurityException, IOException {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
+ try (BufferedSink sink = Okio.buffer(Okio.cipherSink(Okio.sink(file), cipher))) {
+ sink.write(bytes);
+ }
+}
+
+ByteString decryptAesToByteString(File file, byte[] key, byte[] iv)
+ throws GeneralSecurityException, IOException {
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
+ try (BufferedSource source = Okio.buffer(Okio.cipherSource(Okio.source(file), cipher))) {
+ return source.readByteString();
+ }
+}
+```
+
+In Kotlin, these encryption and decryption methods are extensions on `Cipher`:
+
+```kotlin
+fun encryptAes(bytes: ByteString, file: File, key: ByteArray, iv: ByteArray) {
+ val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
+ cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
+ val cipherSink = file.sink().cipherSink(cipher)
+ cipherSink.buffer().use {
+ it.write(bytes)
+ }
+}
+
+fun decryptAesToByteString(file: File, key: ByteArray, iv: ByteArray): ByteString {
+ val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
+ val cipherSource = file.source().cipherSource(cipher)
+ return cipherSource.buffer().use {
+ it.readByteString()
+ }
+}
+```
+
+File System Examples
+--------------------
+
+Okio's recently gained a multiplatform file system API. These examples work on JVM, native, and
+Node.js platforms. In the examples below `fileSystem` is an instance of [FileSystem] such as
+`FileSystem.SYSTEM` or `FakeFileSystem`.
+
+Read all of `readme.md` as a string:
+
+```
+val path = "readme.md".toPath()
+val entireFileString = fileSystem.read(path) {
+ readUtf8()
+}
+```
+
+Read all of `thumbnail.png` as a [ByteString][3]:
+
+```
+val path = "thumbnail.png".toPath()
+val entireFileByteString = fileSystem.read(path) {
+ readByteString()
+}
+```
+
+Read all lines of `/etc/hosts` into a `List<String>`:
+
+```
+val path = "/etc/hosts".toPath()
+val allLines = fileSystem.read(path) {
+ generateSequence { readUtf8Line() }.toList()
+}
+```
+
+Read the prefix of `index.html` that precedes the first `<html>` substring:
+
+```
+val path = "index.html".toPath()
+val untilHtmlTag = fileSystem.read(path) {
+ val htmlTag = indexOf("<html>".encodeUtf8())
+ if (htmlTag != -1L) readUtf8(htmlTag) else null
+}
+```
+
+Write `readme.md` as a string:
+
+```
+val path = "readme.md".toPath()
+fileSystem.write(path) {
+ writeUtf8(
+ """
+ |Hello, World
+ |------------
+ |
+ |This is a sample file.
+ |""".trimMargin()
+ )
+}
+```
+
+Write `data.bin` as a [ByteString][3]:
+
+```
+val path = "data.bin".toPath()
+fileSystem.write(path) {
+ val byteString = "68656c6c6f20776f726c640a".decodeHex()
+ write(byteString)
+}
+```
+
+Write `readme.md` from a `List<String>`:
+
+```
+val path = "readme.md".toPath()
+val lines = listOf(
+ "Hello, World",
+ "------------",
+ "",
+ "This is a sample file.",
+ ""
+)
+fileSystem.write(path) {
+ for (line in lines) {
+ writeUtf8(line)
+ writeUtf8("\n")
+ }
+}
+```
+
+Generate `binary.txt` programmatically:
+
+```
+val path = "binary.txt".toPath()
+fileSystem.write(path) {
+ for (i in 1 until 100) {
+ writeUtf8("$i ${i.toString(2)}")
+ writeUtf8("\n")
+ }
+}
+```
+
+
+Releases
+--------
+
+Our [change log][changelog] has release history.
+
+```kotlin
+implementation("com.squareup.okio:okio:2.10.0")
+```
+
+<details>
+ <summary>Snapshot builds are also available</summary>
+
+```kotlin
+repositories {
+ maven {
+ url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
+ }
+}
+
+dependencies {
+ implementation("com.squareup.okio:okio:2.10.0")
+}
+```
+
+</details>
+
+
+R8 / ProGuard
+--------
+
+If you are using R8 or ProGuard add the options from [this file][proguard].
+
+
+License
+--------
+
+ Copyright 2013 Square, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ [1]: https://github.com/square/okhttp
+ [3]: https://square.github.io/okio/2.x/okio/okio/-byte-string/index.html
+ [4]: https://square.github.io/okio/2.x/okio/okio/-buffer/index.html
+ [5]: https://square.github.io/okio/2.x/okio/okio/-source/index.html
+ [6]: https://square.github.io/okio/2.x/okio/okio/-sink/index.html
+ [7]: https://square.github.io/okio/2.x/okio/okio/-buffered-source/index.html
+ [8]: https://square.github.io/okio/2.x/okio/okio/-buffered-sink/index.html
+ [changelog]: http://square.github.io/okio/changelog/
+ [javadoc]: https://square.github.io/okio/2.x/okio/okio/index.html
+ [nfd]: https://docs.oracle.com/javase/7/docs/api/java/text/Normalizer.Form.html#NFD
+ [nfc]: https://docs.oracle.com/javase/7/docs/api/java/text/Normalizer.Form.html#NFC
+ [base64]: https://tools.ietf.org/html/rfc4648#section-4
+ [bmp]: https://en.wikipedia.org/wiki/BMP_file_format
+ [kotlin]: https://kotlinlang.org/
+ [ok_libraries_talk]: https://www.youtube.com/watch?v=WvyScM_S88c
+ [ok_libraries_slides]: https://speakerdeck.com/jakewharton/a-few-ok-libraries-droidcon-mtl-2015
+ [encoding_talk]: https://www.youtube.com/watch?v=T_p22jMZSrk
+ [encoding_slides]: https://speakerdeck.com/swankjesse/decoding-the-secrets-of-binary-data-droidcon-nyc-2016
+ [ok_multiplatform_talk]: https://www.youtube.com/watch?v=Q8B4eDirgk0
+ [ok_multiplatform_slides]: https://speakerdeck.com/swankjesse/ok-multiplatform
+ [ReadFileLineByLine]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java
+ [ReadFileLineByLineKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/ReadFileLineByLine.kt
+ [WriteFile]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/WriteFile.java
+ [WriteFileKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt
+ [ExploreCharsets]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/ExploreCharsets.java
+ [ExploreCharsetsKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt
+ [FileSystem]: https://square.github.io/okio/2.x/okio/okio/-file-system/index.html
+ [GoldenValue]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/GoldenValue.java
+ [GoldenValueKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt
+ [BitmapEncoder]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java
+ [BitmapEncoderKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt
+ [SocksProxyServer]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/SocksProxyServer.java
+ [SocksProxyServerKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt
+ [Hashing]: https://github.com/square/okio/blob/master/samples/src/jvmMain/java/okio/samples/Hashing.java
+ [HashingKt]: https://github.com/square/okio/blob/master/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt
+ [proguard]: https://github.com/square/okio/blob/master/okio/src/jvmMain/resources/META-INF/proguard/okio.pro
diff --git a/docs/multiplatform.md b/docs/multiplatform.md
new file mode 100644
index 00000000..cad68636
--- /dev/null
+++ b/docs/multiplatform.md
@@ -0,0 +1,40 @@
+Multiplatform
+=============
+
+Okio is a [Kotlin Multiplatform][kotlin_multiplatform] project. We're still completing our feature
+coverage.
+
+
+### Compression (Deflater, Inflater, Gzip)
+
+JVM-only.
+
+
+### Concurrency (Pipe, Timeouts, Throttler)
+
+JVM-only.
+
+Timeout is on all platforms, but only the JVM has a useful implementation.
+
+
+### Core (Buffer, ByteString, Source, Sink)
+
+Available on all platforms.
+
+
+### File System
+
+Available on all platforms. For JavaScript this requires [Node.js][node_js].
+
+
+### Hashing
+
+Okio includes Kotlin implementations of MD5, SHA-1, SHA-256, and SHA-512. This includes both hash
+functions and HMAC functions.
+
+Okio uses the built-in implementations of these functions on the JVM.
+
+
+[kotlin_multiplatform]: https://kotlinlang.org/docs/reference/multiplatform.html
+[mingw]: http://www.mingw.org/
+[node_js]: https://nodejs.org/api/fs.html
diff --git a/docs/releasing.md b/docs/releasing.md
new file mode 100644
index 00000000..94a1a34c
--- /dev/null
+++ b/docs/releasing.md
@@ -0,0 +1,105 @@
+Releasing
+=========
+
+### Prerequisite: Sonatype (Maven Central) Account
+
+Create an account on the [Sonatype issues site][sonatype_issues]. Ask an existing publisher to open
+an issue requesting publishing permissions for `com.squareup` projects.
+
+### Prerequisite: GPG Keys
+
+Generate a GPG key (RSA, 4096 bit, 3650 day) expiry, or use an existing one. You should leave the
+password empty for this key.
+
+```
+$ gpg --full-generate-key
+```
+
+Upload the GPG keys to public servers:
+
+```
+$ gpg --list-keys --keyid-format LONG
+/Users/johnbarber/.gnupg/pubring.kbx
+------------------------------
+pub rsa4096/XXXXXXXXXXXXXXXX 2019-07-16 [SC] [expires: 2029-07-13]
+ YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
+uid [ultimate] John Barber <jbarber@squareup.com>
+sub rsa4096/ZZZZZZZZZZZZZZZZ 2019-07-16 [E] [expires: 2029-07-13]
+
+$ gpg --send-keys --keyserver keyserver.ubuntu.com XXXXXXXXXXXXXXXX
+```
+
+### Prerequisite: Gradle Properties
+
+Define publishing properties in `~/.gradle/gradle.properties`:
+
+```
+signing.keyId=1A2345F8
+signing.password=
+signing.secretKeyRingFile=/Users/jbarber/.gnupg/secring.gpg
+```
+
+`signing.keyId` is the GPG key's ID. Get it with this:
+
+ ```
+ $ gpg --list-keys --keyid-format SHORT
+ ```
+
+`signing.password` is the password for this key. This might be empty!
+
+`signing.secretKeyRingFile` is the absolute path for `secring.gpg`. You may need to export this
+file manually with the following command where `XXXXXXXX` is the `keyId` above:
+
+ ```
+ $ gpg --keyring secring.gpg --export-secret-key XXXXXXXX > ~/.gnupg/secring.gpg
+ ```
+
+
+Cutting a Release
+-----------------
+
+1. Update `CHANGELOG.md`.
+
+2. Set versions:
+
+ ```
+ export RELEASE_VERSION=X.Y.Z
+ export NEXT_VERSION=X.Y.Z-SNAPSHOT
+ ```
+
+3. Set environment variables with your [Sonatype credentials][sonatype_issues].
+
+ ```
+ export SONATYPE_NEXUS_USERNAME=johnbarber
+ export SONATYPE_NEXUS_PASSWORD=`pbpaste`
+ ```
+
+4. Update, build, and upload:
+
+ ```
+ sed -i "" \
+ "s/VERSION_NAME=.*/VERSION_NAME=$RELEASE_VERSION/g" \
+ gradle.properties
+ sed -i "" \
+ "s/\"com.squareup.okio:\([^\:]*\):[^\"]*\"/\"com.squareup.okio:\1:$RELEASE_VERSION\"/g" \
+ `find . -name "README.md"`
+ ./gradlew clean publish
+ ```
+
+5. Visit [Sonatype Nexus][sonatype_nexus] to promote (close then release) the artifact. Or drop it
+ if there is a problem!
+
+6. Tag the release, prepare for the next one, and push to GitHub.
+
+ ```
+ git commit -am "Prepare for release $RELEASE_VERSION."
+ git tag -a parent-$RELEASE_VERSION -m "Version $RELEASE_VERSION"
+ sed -i "" \
+ "s/VERSION_NAME=.*/VERSION_NAME=$NEXT_VERSION/g" \
+ gradle.properties
+ git commit -am "Prepare next development version."
+ git push && git push --tags
+ ```
+
+ [sonatype_issues]: https://issues.sonatype.org/
+ [sonatype_nexus]: https://oss.sonatype.org/
diff --git a/docs/security.md b/docs/security.md
new file mode 100644
index 00000000..1eb81491
--- /dev/null
+++ b/docs/security.md
@@ -0,0 +1,19 @@
+Security Policy
+===============
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ---------- |
+| 2.x | ✅ |
+| 1.x | ✅ |
+
+
+## Reporting a Vulnerability
+
+Square recognizes the important contributions the security research community
+can make. We therefore encourage reporting security issues with the code
+contained in this repository.
+
+If you believe you have discovered a security vulnerability, please follow the
+guidelines at https://bugcrowd.com/squareopensource
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..21f92f07
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,25 @@
+org.gradle.jvmargs='-Dfile.encoding=UTF-8'
+android.enableJetifier=true
+android.useAndroidX=true
+
+# Publishing SHA 256 and 512 hashses of maven-metadata is not supported by Sonatype and Nexus.
+# See https://github.com/gradle/gradle/issues/11308 and
+# https://issues.sonatype.org/browse/NEXUS-21802
+systemProp.org.gradle.internal.publish.checksums.insecure=true
+
+GROUP=com.squareup.okio
+VERSION_NAME=2.11.0-SNAPSHOT
+
+POM_DESCRIPTION=A modern I/O API for Java
+
+POM_URL=https://github.com/square/okio/
+POM_SCM_URL=https://github.com/square/okio/
+POM_SCM_CONNECTION=scm:git:git://github.com/square/okio.git
+POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/okio.git
+
+POM_LICENCE_NAME=The Apache Software License, Version 2.0
+POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
+POM_LICENCE_DIST=repo
+
+POM_DEVELOPER_ID=square
+POM_DEVELOPER_NAME=Square, Inc.
diff --git a/gradle/gradle-mvn-mpp-push.gradle b/gradle/gradle-mvn-mpp-push.gradle
new file mode 100644
index 00000000..7ec94292
--- /dev/null
+++ b/gradle/gradle-mvn-mpp-push.gradle
@@ -0,0 +1,137 @@
+apply plugin: 'maven-publish'
+apply plugin: 'signing'
+apply plugin: 'org.jetbrains.dokka'
+
+def dokkaConfiguration = {
+ outputDirectory.set(file("$rootDir/docs/2.x"))
+
+ dokkaSourceSets {
+ configureEach {
+ reportUndocumented.set(false)
+ skipDeprecated.set(true)
+ jdkVersion.set(8)
+ perPackageOption {
+ matchingRegex.set("com\\.squareup.okio.*")
+ suppress.set(true)
+ }
+ perPackageOption {
+ matchingRegex.set("okio\\.internal.*")
+ suppress.set(true)
+ }
+ }
+ }
+}
+
+dokkaGfm.configure(dokkaConfiguration)
+dokkaHtml.configure(dokkaConfiguration)
+
+def rootRelativePath(path) {
+ return rootProject.file(path).toString().replace('\\', '/')
+}
+
+dokkaHtml.pluginsMapConfiguration.set([
+ "org.jetbrains.dokka.base.DokkaBase": """{ "customStyleSheets": ["${rootRelativePath("docs/css/dokka-logo.css")}"], "customAssets" : ["${rootRelativePath("docs/images/logo-square.png")}"]}"""
+])
+
+def isReleaseBuild() {
+ return VERSION_NAME.contains("SNAPSHOT") == false
+}
+
+def getReleaseRepositoryUrl() {
+ return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL :
+ "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
+}
+
+def getSnapshotRepositoryUrl() {
+ return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL :
+ "https://oss.sonatype.org/content/repositories/snapshots/"
+}
+
+def getRepositoryUsername() {
+ return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : ""
+}
+
+def getRepositoryPassword() {
+ return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : ""
+}
+
+task emptySourcesJar(type: Jar) {
+ classifier = 'sources'
+}
+
+task javadocsJar(type: Jar, dependsOn: dokkaGfm) {
+ classifier = 'javadoc'
+ from dokkaGfm.outputDirectory
+}
+
+signing {
+ required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
+ sign(publishing.publications)
+}
+
+publishing {
+ publications.all {
+ artifact javadocsJar
+
+ pom.withXml {
+ def root = asNode()
+
+ root.children().last() + {
+ resolveStrategy = Closure.DELEGATE_FIRST
+
+ description POM_DESCRIPTION
+ name POM_NAME
+ url POM_URL
+ licenses {
+ license {
+ name POM_LICENCE_NAME
+ url POM_LICENCE_URL
+ distribution POM_LICENCE_DIST
+ }
+ }
+ scm {
+ url POM_SCM_URL
+ connection POM_SCM_CONNECTION
+ developerConnection POM_SCM_DEV_CONNECTION
+ }
+ developers {
+ developer {
+ id POM_DEVELOPER_ID
+ name POM_DEVELOPER_NAME
+ }
+ }
+ }
+ }
+ }
+
+ // Use default artifact name for the JVM target
+ publications {
+ kotlinMultiplatform {
+ artifactId = POM_ARTIFACT_ID + '-multiplatform'
+ }
+ jvm {
+ artifactId = POM_ARTIFACT_ID
+ }
+ }
+
+ afterEvaluate {
+ publications.getByName('kotlinMultiplatform') {
+ // Source jars are only created for platforms, not the common artifact.
+ artifact emptySourcesJar
+ }
+ }
+
+ repositories {
+ maven {
+ url isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl()
+ credentials {
+ username getRepositoryUsername()
+ password getRepositoryPassword()
+ }
+ }
+ maven {
+ name 'test'
+ url "file://${rootProject.buildDir}/localMaven"
+ }
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..e708b1c0
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..4d9ca164
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..4f906e0c
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..ac1b06f9
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..11fd77fb
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,59 @@
+site_name: Okio
+repo_name: Okio
+repo_url: https://github.com/square/okio
+site_description: "A modern I/O library for Android, Kotlin, and Java."
+site_author: Square, Inc.
+remote_branch: gh-pages
+edit_uri: ""
+
+copyright: 'Copyright &copy; 2019 Square, Inc.'
+
+theme:
+ name: 'material'
+ favicon: images/icon-square.png
+ logo: images/icon-square.png
+ palette:
+ primary: 'deep purple'
+ accent: 'white'
+ icon:
+ repo: fontawesome/brands/github
+
+extra:
+ social:
+ - icon: fontawesome/brands/twitter
+ link: https://twitter.com/squareeng
+ - icon: fontawesome/brands/stack-overflow
+ link: https://stackoverflow.com/questions/tagged/okio?sort=active
+
+extra_css:
+ - 'css/app.css'
+
+markdown_extensions:
+ - smarty
+ - codehilite:
+ guess_lang: false
+ - footnotes
+ - meta
+ - toc:
+ permalink: true
+ - pymdownx.betterem:
+ smart_enable: all
+ - pymdownx.caret
+ - pymdownx.inlinehilite
+ - pymdownx.magiclink
+ - pymdownx.smartsymbols
+ - pymdownx.superfences
+ - pymdownx.tabbed
+ - pymdownx.tilde
+ - tables
+
+nav:
+ - 'Overview': index.md
+ - 'Stack Overflow ⏏': https://stackoverflow.com/questions/tagged/okio?sort=active
+ - '2.x API': 2.x/okio/okio/index.html
+ - '1.x API ⏏': https://square.github.io/okio/1.x/okio/
+ - 'Change Log': changelog.md
+ - 'Multiplatform': multiplatform.md
+ - 'Contributing': contributing.md
+ - 'Code of Conduct': code_of_conduct.md
+
diff --git a/okio/build.gradle b/okio/build.gradle
new file mode 100644
index 00000000..980ecb0a
--- /dev/null
+++ b/okio/build.gradle
@@ -0,0 +1,193 @@
+apply plugin: 'org.jetbrains.kotlin.multiplatform'
+
+/*
+ * Here's the main hierarchy of variants. Any `expect` functions in one level of the tree are
+ * `actual` functions in a (potentially indirect) child node.
+ *
+ * ```
+ * common
+ * |-- jvm
+ * '-- nonJvm
+ * |-- js
+ * '-- native
+ * |- unix
+ * | |-- apple
+ * | | |-- iosArm64
+ * | | |-- iosX64
+ * | | |-- macosX64
+ * | | |-- watchosArm32
+ * | | |-- watchosArm64
+ * | | '-- watchosX86
+ * | '-- linux
+ * | '-- linuxX64
+ * '-- mingw
+ * '-- mingwX64
+ * ```
+ *
+ * Every child of `native` also includes a source set that depends on the pointer size:
+ *
+ * * sizet32 for watchOS, including watchOS 64-bit architectures
+ * * sizet64 for everything else
+ *
+ * The `hashFunctions` source set builds on all platforms. It ships as a main source set on non-JVM
+ * platforms and as a test source set on the JVM platform.
+ */
+kotlin {
+ jvm {
+ withJava()
+ }
+ if (kmpJsEnabled) {
+ js {
+ configure([compilations.main, compilations.test]) {
+ tasks.getByName(compileKotlinTaskName).kotlinOptions {
+ moduleKind = "umd"
+ sourceMap = true
+ metaInfo = true
+ }
+ }
+ nodejs {
+ testTask {
+ useMocha {
+ timeout = "30s"
+ }
+ }
+ }
+ }
+ }
+ if (kmpNativeEnabled) {
+ iosX64()
+ iosArm64()
+ watchosArm32()
+ watchosArm64()
+ watchosX86()
+ // Required to generate tests tasks: https://youtrack.jetbrains.com/issue/KT-26547
+ linuxX64()
+ macosX64()
+ mingwX64()
+ }
+ sourceSets {
+ all {
+ languageSettings {
+ useExperimentalAnnotation('kotlin.RequiresOptIn')
+ }
+ }
+ commonMain {
+ dependencies {
+ api deps.kotlin.stdLib.common
+ }
+ }
+ commonTest {
+ dependencies {
+ implementation deps.kotlin.test.common
+ implementation deps.kotlin.test.annotations
+ implementation deps.kotlin.time
+ }
+ }
+ nonJvmMain {
+ kotlin.srcDirs += 'src/hashFunctions/kotlin'
+ }
+ jvmMain {
+ dependencies {
+ api deps.kotlin.stdLib.jdk6
+ compileOnly deps.animalSniffer.annotations
+ }
+ }
+ jvmTest {
+ kotlin.srcDirs += 'src/hashFunctions/kotlin'
+ dependencies {
+ implementation deps.test.junit
+ implementation deps.test.assertj
+ implementation deps.kotlin.test.jdk
+ }
+ }
+ jsMain {
+ dependsOn nonJvmMain
+ dependencies {
+ api deps.kotlin.stdLib.js
+ }
+ }
+ jsTest {
+ dependencies {
+ implementation deps.kotlin.test.js
+ }
+ }
+
+ nativeMain {
+ dependsOn nonJvmMain
+ }
+ nativeTest {
+ dependsOn commonTest
+ }
+
+ sizet32Main {
+ dependsOn nativeMain
+ }
+ sizet64Main {
+ dependsOn nativeMain
+ }
+
+ mingwMain {
+ dependsOn nativeMain
+ }
+ mingwX64Main {
+ dependsOn sizet64Main
+ dependsOn mingwMain
+ }
+ mingwX64Test {
+ dependsOn nativeTest
+ }
+
+ unixMain {
+ dependsOn nativeMain
+ }
+
+ appleMain {
+ dependsOn unixMain
+ }
+ appleTest {
+ dependsOn nativeTest
+ }
+ configure([iosX64Main, iosArm64Main, macosX64Main]) {
+ dependsOn sizet64Main
+ dependsOn appleMain
+ }
+ configure([iosX64Test, iosArm64Test, macosX64Test]) {
+ dependsOn appleTest
+ }
+ configure([watchosArm32Main, watchosArm64Main, watchosX86Main]) {
+ // Note that size_t is 32-bit on all watchOS versions (ie. pointers are always 32-bit).
+ dependsOn sizet32Main
+ dependsOn appleMain
+ }
+ configure([watchosArm32Test, watchosArm64Test, watchosX86Test]) {
+ dependsOn appleTest
+ }
+
+ linuxMain {
+ dependsOn unixMain
+ dependsOn nativeMain
+ }
+ linuxX64Main {
+ dependsOn sizet64Main
+ dependsOn linuxMain
+ }
+ linuxX64Test {
+ dependsOn nativeTest
+ }
+ }
+}
+
+tasks.withType(JavaCompile) {
+ options.encoding = 'UTF-8'
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+// modify these lines for MANIFEST.MF properties or for specific bnd instructions
+project.ext.bndManifest = '''
+ Export-Package: okio
+ Automatic-Module-Name: okio
+ Bundle-SymbolicName: com.squareup.okio
+ '''
+
+apply from: 'jvm/jvm.gradle'
+apply from: "$rootDir/gradle/gradle-mvn-mpp-push.gradle"
diff --git a/okio/gradle.properties b/okio/gradle.properties
new file mode 100644
index 00000000..775966eb
--- /dev/null
+++ b/okio/gradle.properties
@@ -0,0 +1,2 @@
+POM_ARTIFACT_ID=okio
+POM_NAME=Okio
diff --git a/okio/jvm/japicmp/build.gradle b/okio/jvm/japicmp/build.gradle
new file mode 100644
index 00000000..782c6996
--- /dev/null
+++ b/okio/jvm/japicmp/build.gradle
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import me.champeau.gradle.japicmp.JapicmpTask
+
+apply plugin: 'java-library'
+apply plugin: 'me.champeau.gradle.japicmp'
+
+configurations {
+ baseline
+ latest
+}
+
+dependencies {
+ baseline('com.squareup.okio:okio:1.14.1') {
+ transitive = false
+ force = true
+ }
+ latest project(path: ':okio', configuration: 'jvmRuntimeElements')
+}
+
+task japicmp(type: JapicmpTask, dependsOn: 'jar') {
+ oldClasspath = configurations.baseline
+ newClasspath = configurations.latest
+ onlyBinaryIncompatibleModified = true
+ failOnModification = true
+ txtOutputFile = file("$buildDir/reports/japi.txt")
+ ignoreMissingClasses = true
+ includeSynthetic = true
+ classExcludes = [
+ 'okio.ByteString', // Bytecode version changed from 51.0 to 50.0
+ 'okio.RealBufferedSink', // Internal.
+ 'okio.RealBufferedSource', // Internal.
+ 'okio.SegmentedByteString', // Internal.
+ 'okio.SegmentPool', // Internal.
+ 'okio.Util', // Internal.
+ 'okio.Options', // Bytecode version changed from 51.0 to 50.0
+ ]
+ methodExcludes = [
+ 'okio.ByteString#getByte(int)', // Became 'final' in 1.15.0.
+ 'okio.ByteString#size()', // Became 'final' in 1.15.0.
+ ]
+}
+check.dependsOn japicmp
diff --git a/okio/jvm/jmh/README.md b/okio/jvm/jmh/README.md
new file mode 100644
index 00000000..d7c6d779
--- /dev/null
+++ b/okio/jvm/jmh/README.md
@@ -0,0 +1,10 @@
+Okio Benchmarks
+===============
+
+This module contains JMH microbenchmarks. Run benchmarks locally with Gradle:
+
+```
+$ ./gradlew jmh
+```
+
+Select and configure benchmarks in the `jmh` section of `okio/jvm/jmh/build.gradle`.
diff --git a/okio/jvm/jmh/build.gradle b/okio/jvm/jmh/build.gradle
new file mode 100644
index 00000000..54eebdd3
--- /dev/null
+++ b/okio/jvm/jmh/build.gradle
@@ -0,0 +1,36 @@
+import com.github.jengelman.gradle.plugins.shadow.transformers.DontIncludeResourceTransformer
+import com.github.jengelman.gradle.plugins.shadow.transformers.IncludeResourceTransformer
+
+apply plugin: 'java-library'
+apply plugin: 'org.jetbrains.kotlin.jvm'
+apply plugin: 'com.github.johnrengelman.shadow'
+apply plugin: 'me.champeau.gradle.jmh'
+
+jmhJar {
+ def excludeAllBenchmarkLists = new DontIncludeResourceTransformer()
+ excludeAllBenchmarkLists.resource = "META-INF/BenchmarkList"
+ transform(excludeAllBenchmarkLists)
+
+ def includeCorrectBenchmarkList = new IncludeResourceTransformer()
+ includeCorrectBenchmarkList.resource = "META-INF/BenchmarkList"
+ includeCorrectBenchmarkList.file = new File("${project.buildDir}/jmh-generated-resources/META-INF/BenchmarkList")
+ transform(includeCorrectBenchmarkList)
+}
+
+jmh {
+ jvmArgs = ['-Djmh.separateClasspathJAR=true']
+ include = ['com\\.squareup\\.okio\\.benchmarks\\.MessageDigestBenchmark.*']
+ duplicateClassesStrategy = 'warn'
+}
+
+dependencies {
+ compile project(':okio')
+ compile deps.kotlin.stdLib.jdk6
+ compile deps.jmh.core
+ jmh project(path: ':okio', configuration: 'jvmRuntimeElements')
+ jmh deps.kotlin.stdLib.jdk6
+ jmh deps.jmh.core
+ jmh deps.jmh.generator
+}
+
+assemble.dependsOn(jmhJar)
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BenchmarkUtils.kt b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BenchmarkUtils.kt
new file mode 100644
index 00000000..5c7b8116
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BenchmarkUtils.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks
+
+import okio.internal.commonAsUtf8ToByteArray
+import okio.internal.commonToUtf8String
+
+// Necessary to make an invisible functions visible to Java.
+object BenchmarkUtils {
+ @JvmStatic
+ fun ByteArray.decodeUtf8(): String {
+ return commonToUtf8String()
+ }
+
+ @JvmStatic
+ fun String.encodeUtf8(): ByteArray {
+ return commonAsUtf8ToByteArray()
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferCursorSeekBenchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferCursorSeekBenchmark.java
new file mode 100644
index 00000000..8f6c007b
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferCursorSeekBenchmark.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2018 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.RunnerException;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.SampleTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class BufferCursorSeekBenchmark {
+ Buffer buffer;
+ Buffer.UnsafeCursor cursor;
+
+ @Param({ "2097152" })
+ int bufferSize; // 2 MB = 256 Segments
+
+ @Setup
+ public void setup() throws IOException {
+ byte[] source = new byte[8192];
+ buffer = new Buffer();
+ while (buffer.size() < bufferSize) {
+ buffer.write(source);
+ }
+ cursor = new Buffer.UnsafeCursor();
+ }
+
+ @Benchmark
+ public void seekBeginning() {
+ buffer.readUnsafe(cursor);
+ try {
+ cursor.seek(0);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Benchmark
+ public void seekEnd() {
+ buffer.readUnsafe(cursor);
+ try {
+ cursor.seek(buffer.size() - 1);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Benchmark
+ public void seekForward() {
+ buffer.readUnsafe(cursor);
+ try {
+ cursor.seek(0);
+ cursor.seek(1);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Benchmark
+ public void seekBackward() {
+ buffer.readUnsafe(cursor);
+ try {
+ cursor.seek(buffer.size() - 1);
+ cursor.seek(buffer.size() - 2);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public static void main(String[] args) throws IOException, RunnerException {
+ Main.main(new String[] {
+ BufferCursorSeekBenchmark.class.getName()
+ });
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferPerformanceBenchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferPerformanceBenchmark.java
new file mode 100644
index 00000000..22a73a27
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferPerformanceBenchmark.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2014 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okio.benchmarks;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Group;
+import org.openjdk.jmh.annotations.GroupThreads;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Timeout;
+
+import static java.util.Objects.requireNonNull;
+
+@Fork(1)
+@Warmup(iterations = 10, time = 10)
+@Measurement(iterations = 10, time = 10)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+public class BufferPerformanceBenchmark {
+
+ public static final File OriginPath =
+ new File(System.getProperty("okio.bench.origin.path", "/dev/urandom"));
+
+ /* Test Workload
+ *
+ * Each benchmark thread maintains three buffers; a receive buffer, a process buffer
+ * and a send buffer. At every operation:
+ *
+ * - We fill up the receive buffer using the origin, write the request to the process
+ * buffer, and consume the process buffer.
+ * - We fill up the process buffer using the origin, write the response to the send
+ * buffer, and consume the send buffer.
+ *
+ * We use an "origin" source that serves as a preexisting sequence of bytes we can read
+ * from the file system. The request and response bytes are initialized in the beginning
+ * and reused throughout the benchmark in order to eliminate GC effects.
+ *
+ * Typically, we simulate the usage of small reads and large writes. Requests and
+ * responses are satisfied with precomputed buffers to eliminate GC effects on
+ * results.
+ *
+ * There are two types of benchmark tests; hot tests are "pedal to the metal" and
+ * use all CPU they can take. These are useful to magnify performance effects of
+ * changes but are not realistic use cases that should drive optimization efforts.
+ * Cold tests introduce think time between the receiving of the request and sending
+ * of the response. They are more useful as a reasonably realistic workload where
+ * buffers can be read from and written to during request/response handling but
+ * may hide subtle effects of most changes on performance. Prefer to look at the cold
+ * benchmarks first to decide if a bottleneck is worth pursuing, then use the hot
+ * benchmarks to fine tune optimization efforts.
+ *
+ * Benchmark threads do not explicitly communicate between each other (except to sync
+ * iterations as needed by JMH).
+ *
+ * We simulate think time for each benchmark thread by parking the thread for a
+ * configurable number of microseconds (1000 by default).
+ */
+
+
+ @Benchmark
+ @Threads(1)
+ public void threads1hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(2)
+ public void threads2hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(4)
+ public void threads4hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(8)
+ public void threads8hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(16)
+ public void threads16hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(32)
+ public void threads32hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @GroupThreads(1)
+ @Group("cold")
+ public void thinkReadHot(HotBuffers buffers) throws IOException {
+ buffers.receive(requestBytes).readAll(NullSink);
+ }
+
+ @Benchmark
+ @GroupThreads(3)
+ @Group("cold")
+ public void thinkWriteCold(ColdBuffers buffers) throws IOException {
+ buffers.transmit(responseBytes).readAll(NullSink);
+ }
+
+ private void readWriteRecycle(HotBuffers buffers) throws IOException {
+ buffers.receive(requestBytes).readAll(NullSink);
+ buffers.transmit(responseBytes).readAll(NullSink);
+ }
+
+ @Param({ "1000" })
+ int maxThinkMicros = 1000;
+
+ @Param({ "1024" })
+ int maxReadBytes = 1024;
+
+ @Param({ "1024" })
+ int maxWriteBytes = 1024;
+
+ @Param({ "2048" })
+ int requestSize = 2048;
+
+ @Param({ "1" })
+ int responseFactor = 1;
+
+ byte[] requestBytes;
+
+ byte[] responseBytes;
+
+ @Setup(Level.Trial)
+ public void storeRequestResponseData() throws IOException {
+ checkOrigin(OriginPath);
+
+ requestBytes = storeSourceData(new byte[requestSize]);
+ responseBytes = storeSourceData(new byte[requestSize * responseFactor]);
+ }
+
+ private byte[] storeSourceData(byte[] dest) throws IOException {
+ requireNonNull(dest, "dest == null");
+ try (BufferedSource source = Okio.buffer(Okio.source(OriginPath))) {
+ source.readFully(dest);
+ }
+ return dest;
+ }
+
+ private void checkOrigin(File path) throws IOException {
+ requireNonNull(path, "path == null");
+
+ if (!path.canRead()) {
+ throw new IllegalArgumentException("can not access: " + path);
+ }
+
+ try (InputStream in = new FileInputStream(path)) {
+ int available = in.read();
+ if (available < 0) {
+ throw new IllegalArgumentException("can not read: " + path);
+ }
+ }
+ }
+
+ /*
+ * The state class hierarchy is larger than it needs to be due to a JMH
+ * issue where states inheriting setup methods depending on another state
+ * do not get initialized correctly from benchmark methods making use
+ * of groups. To work around, we leave the common setup and teardown code
+ * in superclasses and move the setup method depending on the bench state
+ * to subclasses. Without the workaround, it would have been enough for
+ * `ColdBuffers` to inherit from `HotBuffers`.
+ */
+
+ @State(Scope.Thread)
+ public static class ColdBuffers extends BufferSetup {
+
+ @Setup(Level.Trial)
+ public void setupBench(BufferPerformanceBenchmark bench) {
+ super.bench = bench;
+ }
+
+ @Setup(Level.Invocation)
+ public void lag() throws InterruptedException {
+ TimeUnit.MICROSECONDS.sleep(bench.maxThinkMicros);
+ }
+
+ }
+
+ @State(Scope.Thread)
+ public static class HotBuffers extends BufferSetup {
+
+ @Setup(Level.Trial)
+ public void setupBench(BufferPerformanceBenchmark bench) {
+ super.bench = bench;
+ }
+
+ }
+
+ @State(Scope.Thread)
+ public abstract static class BufferSetup extends BufferState {
+ BufferPerformanceBenchmark bench;
+
+ public BufferedSource receive(byte[] bytes) throws IOException {
+ return super.receive(bytes, bench.maxReadBytes);
+ }
+
+ public BufferedSource transmit(byte[] bytes) throws IOException {
+ return super.transmit(bytes, bench.maxWriteBytes);
+ }
+
+ @TearDown
+ public void dispose() throws IOException {
+ releaseBuffers();
+ }
+
+ }
+
+ public static class BufferState {
+
+ @SuppressWarnings("resource")
+ final Buffer received = new Buffer();
+ @SuppressWarnings("resource")
+ final Buffer sent = new Buffer();
+ @SuppressWarnings("resource")
+ final Buffer process = new Buffer();
+
+ public void releaseBuffers() throws IOException {
+ received.clear();
+ sent.clear();
+ process.clear();
+ }
+
+ /**
+ * Fills up the receive buffer, hands off to process buffer and returns it for consuming.
+ * Expects receive and process buffers to be empty. Leaves the receive buffer empty and
+ * process buffer full.
+ */
+ protected Buffer receive(byte[] bytes, int maxChunkSize) throws IOException {
+ writeChunked(received, bytes, maxChunkSize).readAll(process);
+ return process;
+ }
+
+ /**
+ * Fills up the process buffer, hands off to send buffer and returns it for consuming.
+ * Expects process and sent buffers to be empty. Leaves the process buffer empty and
+ * sent buffer full.
+ */
+ protected BufferedSource transmit(byte[] bytes, int maxChunkSize) throws IOException {
+ writeChunked(process, bytes, maxChunkSize).readAll(sent);
+ return sent;
+ }
+
+ private BufferedSource writeChunked(Buffer buffer, byte[] bytes, final int chunkSize) {
+ int remaining = bytes.length;
+ int offset = 0;
+ while (remaining > 0) {
+ int bytesToWrite = Math.min(remaining, chunkSize);
+ buffer.write(bytes, offset, bytesToWrite);
+ remaining -= bytesToWrite;
+ offset += bytesToWrite;
+ }
+ return buffer;
+ }
+
+ }
+
+ @SuppressWarnings("resource")
+ private static final Sink NullSink = new Sink() {
+
+ @Override public void write(Buffer source, long byteCount) throws EOFException {
+ source.skip(byteCount);
+ }
+
+ @Override public void flush() {
+ // nothing
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ @Override public void close() {
+ // nothing
+ }
+
+ @Override public String toString() {
+ return "NullSink{}";
+ }
+ };
+
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferUtf8Benchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferUtf8Benchmark.java
new file mode 100644
index 00000000..61ea059c
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/BufferUtf8Benchmark.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2018 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.RunnerException;
+
+import okio.Buffer;
+import okio.ByteString;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+public class BufferUtf8Benchmark {
+ private static final Map<String, String> strings = new HashMap<>();
+
+ static {
+ strings.put(
+ "ascii",
+ "Um, I'll tell you the problem with the scientific power that you're using here, "
+ + "it didn't require any discipline to attain it. You read what others had done and you "
+ + "took the next step. You didn't earn the knowledge for yourselves, so you don't take any "
+ + "responsibility for it. You stood on the shoulders of geniuses to accomplish something "
+ + "as fast as you could, and before you even knew what you had, you patented it, and "
+ + "packaged it, and slapped it on a plastic lunchbox, and now you're selling it, you wanna "
+ + "sell it.");
+
+ strings.put(
+ "utf8",
+ "Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 𝛄𝓸𝘂'𝒓𝗲 υ𝖘𝓲𝗇ɡ 𝕙𝚎𝑟e, "
+ + "𝛊𝓽 ⅆ𝕚𝐝𝝿'𝗍 𝔯𝙚𝙦ᴜ𝜾𝒓𝘦 𝔞𝘯𝐲 ԁ𝜄𝑠𝚌ι𝘱lι𝒏e 𝑡𝜎 𝕒𝚝𝖙𝓪і𝞹 𝔦𝚝. 𝒀ο𝗎 𝔯𝑒⍺𝖉 w𝐡𝝰𝔱 𝞂𝞽һ𝓮𝓇ƽ հ𝖺𝖉 ⅾ𝛐𝝅ⅇ 𝝰πԁ 𝔂ᴑᴜ 𝓉ﮨ၀𝚔 "
+ + "т𝒽𝑒 𝗇𝕖ⅹ𝚝 𝔰𝒕е𝓅. 𝘠ⲟ𝖚 𝖉ⅰԁ𝝕'τ 𝙚𝚊r𝞹 𝘵Ꮒ𝖾 𝝒𝐧هwl𝑒𝖉ƍ𝙚 𝓯૦r 𝔂𝞼𝒖𝕣𝑠𝕖l𝙫𝖊𝓼, 𐑈о y𝘰𝒖 ⅆە𝗇't 𝜏α𝒌𝕖 𝛂𝟉ℽ "
+ + "𝐫ⅇ𝗌ⲣ๐ϖ𝖘ꙇᖯ𝓲l𝓲𝒕𝘆 𝐟𝞼𝘳 𝚤𝑡. 𝛶𝛔𝔲 s𝕥σσ𝐝 ﮩ𝕟 𝒕𝗁𝔢 𝘴𝐡𝜎ᴜlⅾ𝓮𝔯𝚜 𝛐𝙛 ᶃ𝚎ᴨᎥս𝚜𝘦𝓈 𝓽𝞸 a𝒄𝚌𝞸mρl𝛊ꜱ𝐡 𝓈𝚘m𝚎𝞃𝔥⍳𝞹𝔤 𝐚𝗌 𝖋a𝐬𝒕 "
+ + "αs γ𝛐𝕦 𝔠ﻫ𝛖lԁ, 𝚊π𝑑 Ь𝑒𝙛૦𝓇𝘦 𝓎٥𝖚 ⅇvℯ𝝅 𝜅ո𝒆w w𝗵𝒂𝘁 ᶌ੦𝗎 h𝐚𝗱, 𝜸ﮨ𝒖 𝓹𝝰𝔱𝖾𝗇𝓽𝔢ⅆ і𝕥, 𝚊𝜛𝓭 𝓹𝖺ⅽϰ𝘢ℊеᏧ 𝑖𝞃, "
+ + "𝐚𝛑ꓒ 𝙨l𝔞р𝘱𝔢𝓭 ɩ𝗍 ہ𝛑 𝕒 pl𝛂ѕᴛ𝗂𝐜 l𝞄ℼ𝔠𝒽𝑏ﮪ⨯, 𝔞ϖ𝒹 n𝛔w 𝛾𝐨𝞄'𝗿𝔢 ꜱ℮ll𝙞nɡ ɩ𝘁, 𝙮𝕠𝛖 w𝑎ℼ𝚗𝛂 𝕤𝓮ll 𝙞𝓉.");
+
+ // The first 't' is actually a '𝓽'
+ strings.put(
+ "sparse",
+ "Um, I'll 𝓽ell you the problem with the scientific power that you're using here, "
+ + "it didn't require any discipline to attain it. You read what others had done and you "
+ + "took the next step. You didn't earn the knowledge for yourselves, so you don't take any "
+ + "responsibility for it. You stood on the shoulders of geniuses to accomplish something "
+ + "as fast as you could, and before you even knew what you had, you patented it, and "
+ + "packaged it, and slapped it on a plastic lunchbox, and now you're selling it, you wanna "
+ + "sell it.");
+
+ strings.put("2bytes", "\u0080\u07ff");
+
+ strings.put("3bytes", "\u0800\ud7ff\ue000\uffff");
+
+ strings.put("4bytes", "\ud835\udeca");
+
+ // high surrogate, 'a', low surrogate, and 'a'
+ strings.put("bad", "\ud800\u0061\udc00\u0061");
+ }
+
+ @Param({"20", "2000", "200000"})
+ int length;
+
+ @Param({"ascii", "utf8", "sparse", "2bytes", "3bytes", "4bytes", "bad"})
+ String encoding;
+
+ Buffer buffer;
+ String encode;
+ ByteString decode;
+
+ @Setup
+ public void setup() {
+ String part = strings.get(encoding);
+
+ // Make all the strings the same length for comparison
+ StringBuilder builder = new StringBuilder(length + 1_000);
+ while (builder.length() < length) {
+ builder.append(part);
+ }
+ builder.setLength(length);
+
+ // Prepare a string and ByteString for encoding and decoding
+ buffer = new Buffer();
+ encode = builder.toString();
+ Buffer temp = new Buffer();
+ temp.writeUtf8(encode);
+ decode = temp.snapshot();
+ }
+
+ @Benchmark
+ public void writeUtf8() {
+ buffer.writeUtf8(encode);
+ buffer.clear();
+ }
+
+ @Benchmark
+ public String readUtf8() {
+ buffer.write(decode);
+ return buffer.readUtf8();
+ }
+
+ public static void main(String[] args) throws IOException, RunnerException {
+ Main.main(new String[] {BufferUtf8Benchmark.class.getName()});
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/GetByteBenchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/GetByteBenchmark.java
new file mode 100644
index 00000000..2a51635a
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/GetByteBenchmark.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.RunnerException;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.SampleTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class GetByteBenchmark {
+ Buffer buffer;
+
+ @Param({ "2097152" })
+ int bufferSize; // 2 MB = 256 Segments
+
+ @Setup
+ public void setup() throws IOException {
+ buffer = new Buffer();
+ while (buffer.size() < bufferSize) {
+ buffer.write(new byte[8192]);
+ }
+ }
+
+ @Benchmark
+ public void getByteBeginning() {
+ buffer.getByte(0);
+ }
+
+ @Benchmark
+ public void getByteEnd() {
+ buffer.getByte(buffer.size() - 1);
+ }
+
+ @Benchmark
+ public void getByteMiddle() {
+ buffer.getByte(buffer.size() / 2);
+ }
+
+ public static void main(String[] args) throws IOException, RunnerException {
+ Main.main(new String[] {
+ GetByteBenchmark.class.getName()
+ });
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/HashFunctionBenchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/HashFunctionBenchmark.java
new file mode 100644
index 00000000..dc117388
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/HashFunctionBenchmark.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class HashFunctionBenchmark {
+
+ MessageDigest jvm;
+
+ @Param({ "100", "1048576" })
+ public int messageSize;
+
+ @Param({ "SHA-1", "SHA-256", "SHA-512", "MD5" })
+ public String algorithm;
+
+ private byte[] message;
+
+ @Setup public void setup() throws NoSuchAlgorithmException {
+ jvm = MessageDigest.getInstance(algorithm);
+ message = new byte[messageSize];
+ }
+
+ @Benchmark public void jvm() {
+ jvm.update(message, 0, messageSize);
+ jvm.digest();
+ }
+
+ public static void main(String[] args) throws IOException {
+ Main.main(new String[] { HashFunctionBenchmark.class.getName() });
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/IndexOfElementBenchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/IndexOfElementBenchmark.java
new file mode 100644
index 00000000..c628b19d
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/IndexOfElementBenchmark.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2016 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import okio.ByteString;
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.RunnerException;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.SampleTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class IndexOfElementBenchmark {
+ ByteString byteString = ByteString.encodeUtf8("abcd");
+ Buffer buffer;
+
+ @Param({ "32768" })
+ int bufferSize;
+
+ @Setup
+ public void setup() throws IOException {
+ buffer = new Buffer()
+ .write(new byte[bufferSize / 2])
+ .write(byteString)
+ .write(new byte[(bufferSize / 2) - byteString.size()]);
+ }
+
+ @Benchmark
+ public void indexOfByte() throws IOException {
+ buffer.indexOf((byte) 'b', 0L);
+ }
+
+ @Benchmark
+ public void indexOfByteString() throws IOException {
+ buffer.indexOf(byteString, 0L);
+ }
+
+ @Benchmark
+ public void indexOfElement() throws IOException {
+ buffer.indexOfElement(byteString, 0L);
+ }
+
+ public static void main(String[] args) throws IOException, RunnerException {
+ Main.main(new String[] {
+ IndexOfElementBenchmark.class.getName()
+ });
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/ReadByteStringBenchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/ReadByteStringBenchmark.java
new file mode 100644
index 00000000..466f7aad
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/ReadByteStringBenchmark.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.RunnerException;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.SampleTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class ReadByteStringBenchmark {
+
+ Buffer buffer;
+
+ @Param({"32768"})
+ int bufferSize;
+
+ @Param({"8", "16", "32", "64", "128", "256", "512", "1024", "2048", "4096", "8192", "16384",
+ "32768"})
+ int byteStringSize;
+
+ @Setup
+ public void setup() {
+ buffer = new Buffer().write(new byte[bufferSize]);
+ }
+
+ @Benchmark
+ public void readByteString() throws IOException {
+ buffer.write(buffer.readByteString(byteStringSize));
+ }
+
+ @Benchmark
+ public void readByteString_toByteArray() throws IOException {
+ buffer.write(buffer.readByteString(byteStringSize).toByteArray());
+ }
+
+ public static void main(String[] args) throws IOException, RunnerException {
+ Main.main(new String[]{
+ ReadByteStringBenchmark.class.getName()
+ });
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/SegmentedByteStringBenchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/SegmentedByteStringBenchmark.java
new file mode 100644
index 00000000..394c2142
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/SegmentedByteStringBenchmark.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2018 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import okio.ByteString;
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.RunnerException;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+public class SegmentedByteStringBenchmark {
+
+ private static final ByteString UNKNOWN = ByteString.encodeUtf8("UNKNOWN");
+ private static final ByteString SEARCH = ByteString.encodeUtf8("tell");
+
+ @Param({"20", "2000", "200000"})
+ int length;
+
+ private ByteString byteString;
+
+ @Setup
+ public void setup() {
+ String part =
+ "Um, I'll tell you the problem with the scientific power that you're using here, "
+ + "it didn't require any discipline to attain it. You read what others had done and you "
+ + "took the next step. You didn't earn the knowledge for yourselves, so you don't take any "
+ + "responsibility for it. You stood on the shoulders of geniuses to accomplish something "
+ + "as fast as you could, and before you even knew what you had, you patented it, and "
+ + "packaged it, and slapped it on a plastic lunchbox, and now you're selling it, you wanna "
+ + "sell it.";
+
+ Buffer buffer = new Buffer();
+ while (buffer.size() < length) {
+ buffer.writeUtf8(part);
+ }
+ byteString = buffer.snapshot(length);
+ }
+
+ @Benchmark
+ public ByteString substring() {
+ return byteString.substring(1, byteString.size() - 1);
+ }
+
+ @Benchmark
+ public ByteString md5() {
+ return byteString.md5();
+ }
+
+ @Benchmark
+ public int indexOfUnknown() {
+ return byteString.indexOf(UNKNOWN);
+ }
+
+ @Benchmark
+ public int lastIndexOfUnknown() {
+ return byteString.lastIndexOf(UNKNOWN);
+ }
+
+ @Benchmark
+ public int indexOfEarly() {
+ return byteString.indexOf(SEARCH);
+ }
+
+ @Benchmark
+ public int lastIndexOfEarly() {
+ return byteString.lastIndexOf(SEARCH);
+ }
+
+ public static void main(String[] args) throws IOException, RunnerException {
+ Main.main(new String[] {SegmentedByteStringBenchmark.class.getName()});
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/SelectBenchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/SelectBenchmark.java
new file mode 100644
index 00000000..eeb72671
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/SelectBenchmark.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import okio.ByteString;
+import okio.Options;
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.RunnerException;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.SampleTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class SelectBenchmark {
+ /** Representative sample field names as one might find in a JSON document. */
+ List<String> sampleValues = Arrays.asList("id", "name", "description", "type", "sku_ids",
+ "offers", "start_time", "end_time", "expires", "start_of_availability", "duration",
+ "allow_recording", "thumbnail_id", "thumbnail_formats", "is_episode", "is_live", "channel_id",
+ "genre_list", "provider_networks", "year", "video_flags", "is_repeat", "series_id",
+ "series_name", "series_description", "original_air_date", "letter_box", "category",
+ "child_protection_rating", "parental_control_minimum_age", "images", "episode_id",
+ "season_number", "episode_number", "directors_list", "scriptwriters_list", "actors_list",
+ "drm_rights", "is_location_chk_reqd", "is_catchup_enabled", "catchup_duration",
+ "is_timeshift_enabled", "timeshift_duration", "is_startover_enabled", "is_recording_enabled",
+ "suspension_time", "shared_ref_id", "linked_channel_number", "audio_lang", "subcategory",
+ "metadata_root_id", "ref_id", "ref_type", "display_position", "thumbnail_format_list",
+ "network", "external_url", "offer_type", "em_format", "em_artist_name", "assets",
+ "media_class", "media_id", "channel_number");
+
+ @Param({ "4", "8", "16", "32", "64" })
+ int optionCount;
+
+ @Param({ "2048" })
+ int selectCount;
+
+ Buffer buffer = new Buffer();
+ Options options;
+ ByteString sampleData;
+
+ @Setup
+ public void setup() throws IOException {
+ ByteString[] byteStrings = new ByteString[optionCount];
+ for (int i = 0; i < optionCount; i++) {
+ byteStrings[i] = ByteString.encodeUtf8(sampleValues.get(i) + "\"");
+ }
+ options = Options.of(byteStrings);
+
+ Random dice = new Random(0);
+ Buffer sampleDataBuffer = new Buffer();
+ for (int i = 0; i < selectCount; i++) {
+ sampleDataBuffer.write(byteStrings[dice.nextInt(optionCount)]);
+ }
+ sampleData = sampleDataBuffer.readByteString();
+ }
+
+ @Benchmark
+ public void select() throws IOException {
+ buffer.write(sampleData);
+ for (int i = 0; i < selectCount; i++) {
+ buffer.select(options);
+ }
+ if (!buffer.exhausted()) throw new AssertionError();
+ }
+
+ public static void main(String[] args) throws IOException, RunnerException {
+ Main.main(new String[] {
+ SelectBenchmark.class.getName()
+ });
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/Utf8Benchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/Utf8Benchmark.java
new file mode 100644
index 00000000..db0b1ef5
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/Utf8Benchmark.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2018 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.RunnerException;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+public class Utf8Benchmark {
+ private static final Charset utf8 = StandardCharsets.UTF_8;
+ private static final Map<String, String> strings = new HashMap<>();
+
+ static {
+ strings.put(
+ "ascii",
+ "Um, I'll tell you the problem with the scientific power that you're using here, "
+ + "it didn't require any discipline to attain it. You read what others had done and you "
+ + "took the next step. You didn't earn the knowledge for yourselves, so you don't take any "
+ + "responsibility for it. You stood on the shoulders of geniuses to accomplish something "
+ + "as fast as you could, and before you even knew what you had, you patented it, and "
+ + "packaged it, and slapped it on a plastic lunchbox, and now you're selling it, you wanna "
+ + "sell it.");
+
+ strings.put(
+ "utf8",
+ "Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 𝛄𝓸𝘂'𝒓𝗲 υ𝖘𝓲𝗇ɡ 𝕙𝚎𝑟e, "
+ + "𝛊𝓽 ⅆ𝕚𝐝𝝿'𝗍 𝔯𝙚𝙦ᴜ𝜾𝒓𝘦 𝔞𝘯𝐲 ԁ𝜄𝑠𝚌ι𝘱lι𝒏e 𝑡𝜎 𝕒𝚝𝖙𝓪і𝞹 𝔦𝚝. 𝒀ο𝗎 𝔯𝑒⍺𝖉 w𝐡𝝰𝔱 𝞂𝞽һ𝓮𝓇ƽ հ𝖺𝖉 ⅾ𝛐𝝅ⅇ 𝝰πԁ 𝔂ᴑᴜ 𝓉ﮨ၀𝚔 "
+ + "т𝒽𝑒 𝗇𝕖ⅹ𝚝 𝔰𝒕е𝓅. 𝘠ⲟ𝖚 𝖉ⅰԁ𝝕'τ 𝙚𝚊r𝞹 𝘵Ꮒ𝖾 𝝒𝐧هwl𝑒𝖉ƍ𝙚 𝓯૦r 𝔂𝞼𝒖𝕣𝑠𝕖l𝙫𝖊𝓼, 𐑈о y𝘰𝒖 ⅆە𝗇't 𝜏α𝒌𝕖 𝛂𝟉ℽ "
+ + "𝐫ⅇ𝗌ⲣ๐ϖ𝖘ꙇᖯ𝓲l𝓲𝒕𝘆 𝐟𝞼𝘳 𝚤𝑡. 𝛶𝛔𝔲 s𝕥σσ𝐝 ﮩ𝕟 𝒕𝗁𝔢 𝘴𝐡𝜎ᴜlⅾ𝓮𝔯𝚜 𝛐𝙛 ᶃ𝚎ᴨᎥս𝚜𝘦𝓈 𝓽𝞸 a𝒄𝚌𝞸mρl𝛊ꜱ𝐡 𝓈𝚘m𝚎𝞃𝔥⍳𝞹𝔤 𝐚𝗌 𝖋a𝐬𝒕 "
+ + "αs γ𝛐𝕦 𝔠ﻫ𝛖lԁ, 𝚊π𝑑 Ь𝑒𝙛૦𝓇𝘦 𝓎٥𝖚 ⅇvℯ𝝅 𝜅ո𝒆w w𝗵𝒂𝘁 ᶌ੦𝗎 h𝐚𝗱, 𝜸ﮨ𝒖 𝓹𝝰𝔱𝖾𝗇𝓽𝔢ⅆ і𝕥, 𝚊𝜛𝓭 𝓹𝖺ⅽϰ𝘢ℊеᏧ 𝑖𝞃, "
+ + "𝐚𝛑ꓒ 𝙨l𝔞р𝘱𝔢𝓭 ɩ𝗍 ہ𝛑 𝕒 pl𝛂ѕᴛ𝗂𝐜 l𝞄ℼ𝔠𝒽𝑏ﮪ⨯, 𝔞ϖ𝒹 n𝛔w 𝛾𝐨𝞄'𝗿𝔢 ꜱ℮ll𝙞nɡ ɩ𝘁, 𝙮𝕠𝛖 w𝑎ℼ𝚗𝛂 𝕤𝓮ll 𝙞𝓉.");
+
+ // The first 't' is actually a '𝓽'
+ strings.put(
+ "sparse",
+ "Um, I'll 𝓽ell you the problem with the scientific power that you're using here, "
+ + "it didn't require any discipline to attain it. You read what others had done and you "
+ + "took the next step. You didn't earn the knowledge for yourselves, so you don't take any "
+ + "responsibility for it. You stood on the shoulders of geniuses to accomplish something "
+ + "as fast as you could, and before you even knew what you had, you patented it, and "
+ + "packaged it, and slapped it on a plastic lunchbox, and now you're selling it, you wanna "
+ + "sell it.");
+
+ strings.put("2bytes", "\u0080\u07ff");
+
+ strings.put("3bytes", "\u0800\ud7ff\ue000\uffff");
+
+ strings.put("4bytes", "\ud835\udeca");
+
+ // high surrogate, 'a', low surrogate, and 'a'
+ strings.put("bad", "\ud800\u0061\udc00\u0061");
+ }
+
+ @Param({"20", "2000", "200000"})
+ int length;
+
+ @Param({"ascii", "utf8", "sparse", "2bytes", "3bytes", "4bytes", "bad"})
+ String encoding;
+
+ String encode;
+ byte[] decodeArray;
+
+ @Setup
+ public void setup() {
+ String part = strings.get(encoding);
+
+ // Make all the strings the same length for comparison
+ StringBuilder builder = new StringBuilder(length + 1_000);
+ while (builder.length() < length) {
+ builder.append(part);
+ }
+ builder.setLength(length);
+
+ // Prepare a string and byte array for encoding and decoding
+ encode = builder.toString();
+ decodeArray = encode.getBytes(utf8);
+ }
+
+ @Benchmark
+ public byte[] stringToBytesOkio() {
+ return BenchmarkUtils.encodeUtf8(encode);
+ }
+
+ @Benchmark
+ public byte[] stringToBytesJava() {
+ return encode.getBytes(utf8);
+ }
+
+ @Benchmark
+ public String bytesToStringOkio() {
+ // For ASCII only decoding, this will never be faster than Java. Because
+ // Java can trust the decoded char array and it will be the correct size for
+ // ASCII, it is able to avoid the extra defensive copy Okio is forced to
+ // make because it doesn't have access to String internals.
+ return BenchmarkUtils.decodeUtf8(decodeArray);
+ }
+
+ @Benchmark
+ public String bytesToStringJava() {
+ return new String(decodeArray, utf8);
+ }
+
+ public static void main(String[] args) throws IOException, RunnerException {
+ Main.main(new String[] {Utf8Benchmark.class.getName()});
+ }
+}
diff --git a/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/WriteHexadecimalBenchmark.java b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/WriteHexadecimalBenchmark.java
new file mode 100644
index 00000000..4cc1fb64
--- /dev/null
+++ b/okio/jvm/jmh/src/jmh/java/com/squareup/okio/benchmarks/WriteHexadecimalBenchmark.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okio.benchmarks;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import org.openjdk.jmh.Main;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.RunnerException;
+
+@Fork(1)
+@Warmup(iterations = 5, time = 2)
+@Measurement(iterations = 5, time = 2)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class WriteHexadecimalBenchmark {
+
+ Buffer buffer;
+
+ @Param({"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"})
+ int width;
+
+ @Setup
+ public void setup() {
+ buffer = new Buffer();
+ }
+
+ @TearDown(Level.Invocation)
+ public void teardown() {
+ buffer.clear();
+ }
+
+ @Benchmark
+ public void writeHex() {
+ buffer.writeHexadecimalUnsignedLong(1L << width);
+ }
+
+ public static void main(String[] args) throws IOException, RunnerException {
+ Main.main(new String[]{
+ WriteHexadecimalBenchmark.class.getName()
+ });
+ }
+}
diff --git a/okio/jvm/jvm.gradle b/okio/jvm/jvm.gradle
new file mode 100644
index 00000000..cd81891e
--- /dev/null
+++ b/okio/jvm/jvm.gradle
@@ -0,0 +1,26 @@
+apply plugin: 'java-library'
+apply plugin: 'ru.vyarus.animalsniffer'
+
+kotlin.targets.matching { it.platformType.name == 'jvm' }.all { target ->
+ target.project.sourceCompatibility = JavaVersion.VERSION_1_7
+ target.project.targetCompatibility = JavaVersion.VERSION_1_7
+
+ tasks['jvmJar'].configure { t ->
+ // the bnd task convention modifies this jar task accordingly
+ def bndConvention = bndBundleTaskConventionClass.newInstance(t);
+ bndConvention.bnd = project.ext.bndManifest
+ // call the convention when the task has finished to modify the jar to contain OSGi metadata
+ t.doLast {
+ bndConvention.buildBundle()
+ }
+ }
+
+ target.project.animalsniffer {
+ sourceSets = [target.project.sourceSets.main]
+ }
+
+ target.project.dependencies {
+ signature 'net.sf.androidscents.signature:android-api-level-15:4.0.3_r5@signature'
+ signature 'org.codehaus.mojo.signature:java17:1.0@signature'
+ }
+}
diff --git a/okio/src/appleMain/kotlin/okio/ByteString.kt b/okio/src/appleMain/kotlin/okio/ByteString.kt
new file mode 100644
index 00000000..eb141033
--- /dev/null
+++ b/okio/src/appleMain/kotlin/okio/ByteString.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.cinterop.addressOf
+import kotlinx.cinterop.usePinned
+import platform.Foundation.NSData
+import platform.posix.memcpy
+
+fun NSData.toByteString(): ByteString {
+ val data = this
+ return ByteString(
+ ByteArray(data.length.toInt()).apply {
+ usePinned { pinned ->
+ memcpy(pinned.addressOf(0), data.bytes, data.length)
+ }
+ }
+ )
+}
diff --git a/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt b/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt
new file mode 100644
index 00000000..5ff03b21
--- /dev/null
+++ b/okio/src/appleTest/kotlin/okio/AppleByteStringTest.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import platform.Foundation.NSData
+import platform.Foundation.NSString
+import platform.Foundation.NSUTF8StringEncoding
+import platform.Foundation.dataUsingEncoding
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class AppleByteStringTest {
+ @Test fun nsDataToByteString() {
+ val data = ("Hello" as NSString).dataUsingEncoding(NSUTF8StringEncoding) as NSData
+ val byteString = data.toByteString()
+ assertEquals("Hello", byteString.utf8())
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/-Base64.kt b/okio/src/commonMain/kotlin/okio/-Base64.kt
new file mode 100644
index 00000000..98db0b5a
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/-Base64.kt
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("-Base64")
+package okio
+
+import okio.ByteString.Companion.encodeUtf8
+import kotlin.jvm.JvmName
+
+/** @author Alexander Y. Kleymenov */
+
+internal val BASE64 =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".encodeUtf8().data
+internal val BASE64_URL_SAFE =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".encodeUtf8().data
+
+internal fun String.decodeBase64ToArray(): ByteArray? {
+ // Ignore trailing '=' padding and whitespace from the input.
+ var limit = length
+ while (limit > 0) {
+ val c = this[limit - 1]
+ if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') {
+ break
+ }
+ limit--
+ }
+
+ // If the input includes whitespace, this output array will be longer than necessary.
+ val out = ByteArray((limit * 6L / 8L).toInt())
+ var outCount = 0
+ var inCount = 0
+
+ var word = 0
+ for (pos in 0 until limit) {
+ val c = this[pos]
+
+ val bits: Int
+ if (c in 'A'..'Z') {
+ // char ASCII value
+ // A 65 0
+ // Z 90 25 (ASCII - 65)
+ bits = c.toInt() - 65
+ } else if (c in 'a'..'z') {
+ // char ASCII value
+ // a 97 26
+ // z 122 51 (ASCII - 71)
+ bits = c.toInt() - 71
+ } else if (c in '0'..'9') {
+ // char ASCII value
+ // 0 48 52
+ // 9 57 61 (ASCII + 4)
+ bits = c.toInt() + 4
+ } else if (c == '+' || c == '-') {
+ bits = 62
+ } else if (c == '/' || c == '_') {
+ bits = 63
+ } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') {
+ continue
+ } else {
+ return null
+ }
+
+ // Append this char's 6 bits to the word.
+ word = word shl 6 or bits
+
+ // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes.
+ inCount++
+ if (inCount % 4 == 0) {
+ out[outCount++] = (word shr 16).toByte()
+ out[outCount++] = (word shr 8).toByte()
+ out[outCount++] = word.toByte()
+ }
+ }
+
+ val lastWordChars = inCount % 4
+ when (lastWordChars) {
+ 1 -> {
+ // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail.
+ return null
+ }
+ 2 -> {
+ // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits.
+ word = word shl 12
+ out[outCount++] = (word shr 16).toByte()
+ }
+ 3 -> {
+ // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits.
+ word = word shl 6
+ out[outCount++] = (word shr 16).toByte()
+ out[outCount++] = (word shr 8).toByte()
+ }
+ }
+
+ // If we sized our out array perfectly, we're done.
+ if (outCount == out.size) return out
+
+ // Copy the decoded bytes to a new, right-sized array.
+ return out.copyOf(outCount)
+}
+
+internal fun ByteArray.encodeBase64(map: ByteArray = BASE64): String {
+ val length = (size + 2) / 3 * 4
+ val out = ByteArray(length)
+ var index = 0
+ val end = size - size % 3
+ var i = 0
+ while (i < end) {
+ val b0 = this[i++].toInt()
+ val b1 = this[i++].toInt()
+ val b2 = this[i++].toInt()
+ out[index++] = map[(b0 and 0xff shr 2)]
+ out[index++] = map[(b0 and 0x03 shl 4) or (b1 and 0xff shr 4)]
+ out[index++] = map[(b1 and 0x0f shl 2) or (b2 and 0xff shr 6)]
+ out[index++] = map[(b2 and 0x3f)]
+ }
+ when (size - end) {
+ 1 -> {
+ val b0 = this[i].toInt()
+ out[index++] = map[b0 and 0xff shr 2]
+ out[index++] = map[b0 and 0x03 shl 4]
+ out[index++] = '='.toByte()
+ out[index] = '='.toByte()
+ }
+ 2 -> {
+ val b0 = this[i++].toInt()
+ val b1 = this[i].toInt()
+ out[index++] = map[(b0 and 0xff shr 2)]
+ out[index++] = map[(b0 and 0x03 shl 4) or (b1 and 0xff shr 4)]
+ out[index++] = map[(b1 and 0x0f shl 2)]
+ out[index] = '='.toByte()
+ }
+ }
+ return out.toUtf8String()
+}
diff --git a/okio/src/commonMain/kotlin/okio/-Platform.kt b/okio/src/commonMain/kotlin/okio/-Platform.kt
new file mode 100644
index 00000000..4790d3cd
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/-Platform.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+internal expect fun ByteArray.toUtf8String(): String
+
+internal expect fun String.asUtf8ToByteArray(): ByteArray
+
+// TODO make internal https://youtrack.jetbrains.com/issue/KT-37316
+expect class ArrayIndexOutOfBoundsException(message: String?) : IndexOutOfBoundsException
+
+internal expect inline fun <R> synchronized(lock: Any, block: () -> R): R
+
+expect open class IOException(message: String?, cause: Throwable?) : Exception {
+ constructor(message: String? = null)
+}
+
+expect open class EOFException(message: String? = null) : IOException
+
+expect class FileNotFoundException(message: String? = null) : IOException
+
+expect interface Closeable {
+ /**
+ * Closes this object and releases the resources it holds. It is an error to use an object after
+ * it has been closed. It is safe to close an object more than once.
+ */
+ @Throws(IOException::class)
+ fun close()
+}
diff --git a/okio/src/commonMain/kotlin/okio/-Util.kt b/okio/src/commonMain/kotlin/okio/-Util.kt
new file mode 100644
index 00000000..9ab36882
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/-Util.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("-Util")
+
+package okio
+
+import okio.internal.HEX_DIGIT_CHARS
+import kotlin.jvm.JvmName
+
+internal fun checkOffsetAndCount(size: Long, offset: Long, byteCount: Long) {
+ if (offset or byteCount < 0 || offset > size || size - offset < byteCount) {
+ throw ArrayIndexOutOfBoundsException("size=$size offset=$offset byteCount=$byteCount")
+ }
+}
+
+/* ktlint-disable no-multi-spaces indent */
+
+internal fun Short.reverseBytes(): Short {
+ val i = toInt() and 0xffff
+ val reversed = (i and 0xff00 ushr 8) or
+ (i and 0x00ff shl 8)
+ return reversed.toShort()
+}
+
+internal fun Int.reverseBytes(): Int {
+ return (this and -0x1000000 ushr 24) or
+ (this and 0x00ff0000 ushr 8) or
+ (this and 0x0000ff00 shl 8) or
+ (this and 0x000000ff shl 24)
+}
+
+internal fun Long.reverseBytes(): Long {
+ return (this and -0x100000000000000L ushr 56) or
+ (this and 0x00ff000000000000L ushr 40) or
+ (this and 0x0000ff0000000000L ushr 24) or
+ (this and 0x000000ff00000000L ushr 8) or
+ (this and 0x00000000ff000000L shl 8) or
+ (this and 0x0000000000ff0000L shl 24) or
+ (this and 0x000000000000ff00L shl 40) or
+ (this and 0x00000000000000ffL shl 56)
+}
+
+/* ktlint-enable no-multi-spaces indent */
+
+internal inline infix fun Int.leftRotate(bitCount: Int): Int {
+ return (this shl bitCount) or (this ushr (32 - bitCount))
+}
+
+internal inline infix fun Long.rightRotate(bitCount: Int): Long {
+ return (this ushr bitCount) or (this shl (64 - bitCount))
+}
+
+@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
+internal inline infix fun Byte.shr(other: Int): Int = toInt() shr other
+
+@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
+internal inline infix fun Byte.shl(other: Int): Int = toInt() shl other
+
+@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
+internal inline infix fun Byte.and(other: Int): Int = toInt() and other
+
+@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
+internal inline infix fun Byte.and(other: Long): Long = toLong() and other
+
+@Suppress("NOTHING_TO_INLINE") // Pending `kotlin.experimental.xor` becoming stable
+internal inline infix fun Byte.xor(other: Byte): Byte = (toInt() xor other.toInt()).toByte()
+
+@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
+internal inline infix fun Int.and(other: Long): Long = toLong() and other
+
+@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
+internal inline fun minOf(a: Long, b: Int): Long = minOf(a, b.toLong())
+
+@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
+internal inline fun minOf(a: Int, b: Long): Long = minOf(a.toLong(), b)
+
+internal fun arrayRangeEquals(
+ a: ByteArray,
+ aOffset: Int,
+ b: ByteArray,
+ bOffset: Int,
+ byteCount: Int
+): Boolean {
+ for (i in 0 until byteCount) {
+ if (a[i + aOffset] != b[i + bOffset]) return false
+ }
+ return true
+}
+
+internal fun Byte.toHexString(): String {
+ val result = CharArray(2)
+ result[0] = HEX_DIGIT_CHARS[this shr 4 and 0xf]
+ result[1] = HEX_DIGIT_CHARS[this and 0xf] // ktlint-disable no-multi-spaces
+ return String(result)
+}
+
+internal fun Int.toHexString(): String {
+ if (this == 0) return "0" // Required as code below does not handle 0
+
+ val result = CharArray(8)
+ result[0] = HEX_DIGIT_CHARS[this shr 28 and 0xf]
+ result[1] = HEX_DIGIT_CHARS[this shr 24 and 0xf]
+ result[2] = HEX_DIGIT_CHARS[this shr 20 and 0xf]
+ result[3] = HEX_DIGIT_CHARS[this shr 16 and 0xf]
+ result[4] = HEX_DIGIT_CHARS[this shr 12 and 0xf]
+ result[5] = HEX_DIGIT_CHARS[this shr 8 and 0xf] // ktlint-disable no-multi-spaces
+ result[6] = HEX_DIGIT_CHARS[this shr 4 and 0xf] // ktlint-disable no-multi-spaces
+ result[7] = HEX_DIGIT_CHARS[this and 0xf] // ktlint-disable no-multi-spaces
+
+ // Find the first non-zero index
+ var i = 0
+ while (i < result.size) {
+ if (result[i] != '0') break
+ i++
+ }
+
+ return String(result, i, result.size - i)
+}
+
+internal fun Long.toHexString(): String {
+ if (this == 0L) return "0" // Required as code below does not handle 0
+
+ val result = CharArray(16)
+ result[ 0] = HEX_DIGIT_CHARS[(this shr 60 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[ 1] = HEX_DIGIT_CHARS[(this shr 56 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[ 2] = HEX_DIGIT_CHARS[(this shr 52 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[ 3] = HEX_DIGIT_CHARS[(this shr 48 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[ 4] = HEX_DIGIT_CHARS[(this shr 44 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[ 5] = HEX_DIGIT_CHARS[(this shr 40 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[ 6] = HEX_DIGIT_CHARS[(this shr 36 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[ 7] = HEX_DIGIT_CHARS[(this shr 32 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[ 8] = HEX_DIGIT_CHARS[(this shr 28 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[ 9] = HEX_DIGIT_CHARS[(this shr 24 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[10] = HEX_DIGIT_CHARS[(this shr 20 and 0xf).toInt()]
+ result[11] = HEX_DIGIT_CHARS[(this shr 16 and 0xf).toInt()]
+ result[12] = HEX_DIGIT_CHARS[(this shr 12 and 0xf).toInt()]
+ result[13] = HEX_DIGIT_CHARS[(this shr 8 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[14] = HEX_DIGIT_CHARS[(this shr 4 and 0xf).toInt()] // ktlint-disable no-multi-spaces
+ result[15] = HEX_DIGIT_CHARS[(this and 0xf).toInt()] // ktlint-disable no-multi-spaces
+
+ // Find the first non-zero index
+ var i = 0
+ while (i < result.size) {
+ if (result[i] != '0') break
+ i++
+ }
+
+ return String(result, i, result.size - i)
+}
diff --git a/okio/src/commonMain/kotlin/okio/Buffer.kt b/okio/src/commonMain/kotlin/okio/Buffer.kt
new file mode 100644
index 00000000..16394d2c
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/Buffer.kt
@@ -0,0 +1,408 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlin.jvm.JvmField
+
+/**
+ * A collection of bytes in memory.
+ *
+ * **Moving data from one buffer to another is fast.** Instead of copying bytes from one place in
+ * memory to another, this class just changes ownership of the underlying byte arrays.
+ *
+ * **This buffer grows with your data.** Just like ArrayList, each buffer starts small. It consumes
+ * only the memory it needs to.
+ *
+ * **This buffer pools its byte arrays.** When you allocate a byte array in Java, the runtime must
+ * zero-fill the requested array before returning it to you. Even if you're going to write over that
+ * space anyway. This class avoids zero-fill and GC churn by pooling byte arrays.
+ */
+expect class Buffer() : BufferedSource, BufferedSink {
+ internal var head: Segment?
+
+ var size: Long
+ internal set
+
+ override val buffer: Buffer
+
+ override fun emitCompleteSegments(): Buffer
+
+ override fun emit(): Buffer
+
+ /** Copy `byteCount` bytes from this, starting at `offset`, to `out`. */
+ fun copyTo(
+ out: Buffer,
+ offset: Long = 0L,
+ byteCount: Long
+ ): Buffer
+
+ /**
+ * Overload of [copyTo] with byteCount = size - offset, work around for
+ * https://youtrack.jetbrains.com/issue/KT-30847
+ */
+ fun copyTo(
+ out: Buffer,
+ offset: Long = 0L
+ ): Buffer
+
+ /**
+ * Returns the number of bytes in segments that are not writable. This is the number of bytes that
+ * can be flushed immediately to an underlying sink without harming throughput.
+ */
+ fun completeSegmentByteCount(): Long
+
+ /** Returns the byte at `pos`. */
+ operator fun get(pos: Long): Byte
+
+ /**
+ * Discards all bytes in this buffer. Calling this method when you're done with a buffer will
+ * return its segments to the pool.
+ */
+ fun clear()
+
+ /** Discards `byteCount` bytes from the head of this buffer. */
+ override fun skip(byteCount: Long)
+
+ override fun write(byteString: ByteString): Buffer
+
+ override fun write(byteString: ByteString, offset: Int, byteCount: Int): Buffer
+
+ override fun writeUtf8(string: String): Buffer
+
+ override fun writeUtf8(string: String, beginIndex: Int, endIndex: Int): Buffer
+
+ override fun writeUtf8CodePoint(codePoint: Int): Buffer
+
+ override fun write(source: ByteArray): Buffer
+
+ /**
+ * Returns a tail segment that we can write at least `minimumCapacity`
+ * bytes to, creating it if necessary.
+ */
+ internal fun writableSegment(minimumCapacity: Int): Segment
+
+ fun md5(): ByteString
+
+ fun sha1(): ByteString
+
+ fun sha256(): ByteString
+
+ fun sha512(): ByteString
+
+ /** Returns the 160-bit SHA-1 HMAC of this buffer. */
+ fun hmacSha1(key: ByteString): ByteString
+
+ /** Returns the 256-bit SHA-256 HMAC of this buffer. */
+ fun hmacSha256(key: ByteString): ByteString
+
+ /** Returns the 512-bit SHA-512 HMAC of this buffer. */
+ fun hmacSha512(key: ByteString): ByteString
+
+ override fun write(source: ByteArray, offset: Int, byteCount: Int): Buffer
+
+ override fun write(source: Source, byteCount: Long): Buffer
+
+ override fun writeByte(b: Int): Buffer
+
+ override fun writeShort(s: Int): Buffer
+
+ override fun writeShortLe(s: Int): Buffer
+
+ override fun writeInt(i: Int): Buffer
+
+ override fun writeIntLe(i: Int): Buffer
+
+ override fun writeLong(v: Long): Buffer
+
+ override fun writeLongLe(v: Long): Buffer
+
+ override fun writeDecimalLong(v: Long): Buffer
+
+ override fun writeHexadecimalUnsignedLong(v: Long): Buffer
+
+ /** Returns a deep copy of this buffer. */
+ fun copy(): Buffer
+
+ /** Returns an immutable copy of this buffer as a byte string. */
+ fun snapshot(): ByteString
+
+ /** Returns an immutable copy of the first `byteCount` bytes of this buffer as a byte string. */
+ fun snapshot(byteCount: Int): ByteString
+
+ fun readUnsafe(unsafeCursor: UnsafeCursor = UnsafeCursor()): UnsafeCursor
+
+ fun readAndWriteUnsafe(unsafeCursor: UnsafeCursor = UnsafeCursor()): UnsafeCursor
+
+ /**
+ * A handle to the underlying data in a buffer. This handle is unsafe because it does not enforce
+ * its own invariants. Instead, it assumes a careful user who has studied Okio's implementation
+ * details and their consequences.
+ *
+ * Buffer Internals
+ * ----------------
+ *
+ * Most code should use `Buffer` as a black box: a class that holds 0 or more bytes of
+ * data with efficient APIs to append data to the end and to consume data from the front. Usually
+ * this is also the most efficient way to use buffers because it allows Okio to employ several
+ * optimizations, including:
+ *
+ * * **Fast Allocation:** Buffers use a shared pool of memory that is not zero-filled before use.
+ * * **Fast Resize:** A buffer's capacity can change without copying its contents.
+ * * **Fast Move:** Memory ownership can be reassigned from one buffer to another.
+ * * **Fast Copy:** Multiple buffers can share the same underlying memory.
+ * * **Fast Encoding and Decoding:** Common operations like UTF-8 encoding and decimal decoding
+ * do not require intermediate objects to be allocated.
+ *
+ * These optimizations all leverage the way Okio stores data internally. Okio Buffers are
+ * implemented using a doubly-linked list of segments. Each segment is a contiguous range within a
+ * 8 KiB `ByteArray`. Each segment has two indexes, `start`, the offset of the first byte of the
+ * array containing application data, and `end`, the offset of the first byte beyond `start` whose
+ * data is undefined.
+ *
+ * New buffers are empty and have no segments:
+ *
+ * ```
+ * val buffer = Buffer()
+ * ```
+ *
+ * We append 7 bytes of data to the end of our empty buffer. Internally, the buffer allocates a
+ * segment and writes its new data there. The lone segment has an 8 KiB byte array but only 7
+ * bytes of data:
+ *
+ * ```
+ * buffer.writeUtf8("sealion")
+ *
+ * // [ 's', 'e', 'a', 'l', 'i', 'o', 'n', '?', '?', '?', ...]
+ * // ^ ^
+ * // start = 0 end = 7
+ * ```
+ *
+ * When we read 4 bytes of data from the buffer, it finds its first segment and returns that data
+ * to us. As bytes are read the data is consumed. The segment tracks this by adjusting its
+ * internal indices.
+ *
+ * ```
+ * buffer.readUtf8(4) // "seal"
+ *
+ * // [ 's', 'e', 'a', 'l', 'i', 'o', 'n', '?', '?', '?', ...]
+ * // ^ ^
+ * // start = 4 end = 7
+ * ```
+ *
+ * As we write data into a buffer we fill up its internal segments. When a write doesn't fit into
+ * a buffer's last segment, additional segments are allocated and appended to the linked list of
+ * segments. Each segment has its own start and end indexes tracking where the user's data begins
+ * and ends.
+ *
+ * ```
+ * val xoxo = new Buffer()
+ * xoxo.writeUtf8("xo".repeat(5_000))
+ *
+ * // [ 'x', 'o', 'x', 'o', 'x', 'o', 'x', 'o', ..., 'x', 'o', 'x', 'o']
+ * // ^ ^
+ * // start = 0 end = 8192
+ * //
+ * // [ 'x', 'o', 'x', 'o', ..., 'x', 'o', 'x', 'o', '?', '?', '?', ...]
+ * // ^ ^
+ * // start = 0 end = 1808
+ * ```
+ *
+ * The start index is always **inclusive** and the end index is always **exclusive**. The data
+ * preceding the start index is undefined, and the data at and following the end index is
+ * undefined.
+ *
+ * After the last byte of a segment has been read, that segment may be returned to an internal
+ * segment pool. In addition to reducing the need to do garbage collection, segment pooling also
+ * saves the JVM from needing to zero-fill byte arrays. Okio doesn't need to zero-fill its arrays
+ * because it always writes memory before it reads it. But if you look at a segment in a debugger
+ * you may see its effects. In this example, one of the "xoxo" segments above is reused in an
+ * unrelated buffer:
+ *
+ * ```
+ * val abc = new Buffer()
+ * abc.writeUtf8("abc")
+ *
+ * // [ 'a', 'b', 'c', 'o', 'x', 'o', 'x', 'o', ...]
+ * // ^ ^
+ * // start = 0 end = 3
+ * ```
+ *
+ * There is an optimization in `Buffer.clone()` and other methods that allows two segments to
+ * share the same underlying byte array. Clones can't write to the shared byte array; instead they
+ * allocate a new (private) segment early.
+ *
+ * ```
+ * val nana = new Buffer()
+ * nana.writeUtf8("na".repeat(2_500))
+ * nana.readUtf8(2) // "na"
+ *
+ * // [ 'n', 'a', 'n', 'a', ..., 'n', 'a', 'n', 'a', '?', '?', '?', ...]
+ * // ^ ^
+ * // start = 2 end = 5000
+ *
+ * nana2 = nana.clone()
+ * nana2.writeUtf8("batman")
+ *
+ * // [ 'n', 'a', 'n', 'a', ..., 'n', 'a', 'n', 'a', '?', '?', '?', ...]
+ * // ^ ^
+ * // start = 2 end = 5000
+ * //
+ * // [ 'b', 'a', 't', 'm', 'a', 'n', '?', '?', '?', ...]
+ * // ^ ^
+ * // start = 0 end = 6
+ * ```
+ *
+ * Segments are not shared when the shared region is small (ie. less than 1 KiB). This is intended
+ * to prevent fragmentation in sharing-heavy use cases.
+ *
+ * Unsafe Cursor API
+ * -----------------
+ *
+ * This class exposes privileged access to the internal byte arrays of a buffer. A cursor either
+ * references the data of a single segment, it is before the first segment (`offset == -1`), or it
+ * is after the last segment (`offset == buffer.size`).
+ *
+ * Call [UnsafeCursor.seek] to move the cursor to the segment that contains a specified offset.
+ * After seeking, [UnsafeCursor.data] references the segment's internal byte array,
+ * [UnsafeCursor.start] is the segment's start and [UnsafeCursor.end] is its end.
+ *
+ * Call [UnsafeCursor.next] to advance the cursor to the next segment. This returns -1 if there
+ * are no further segments in the buffer.
+ *
+ * Use [Buffer.readUnsafe] to create a cursor to read buffer data and [Buffer.readAndWriteUnsafe]
+ * to create a cursor to read and write buffer data. In either case, always call
+ * [UnsafeCursor.close] when done with a cursor. This is convenient with Kotlin's
+ * [use] extension function. In this example we read all of the bytes in a buffer into a byte
+ * array:
+ *
+ * ```
+ * val bufferBytes = ByteArray(buffer.size.toInt())
+ *
+ * buffer.readUnsafe().use { cursor ->
+ * while (cursor.next() != -1) {
+ * System.arraycopy(cursor.data, cursor.start,
+ * bufferBytes, cursor.offset.toInt(), cursor.end - cursor.start);
+ * }
+ * }
+ * ```
+ *
+ * Change the capacity of a buffer with [resizeBuffer]. This is only permitted for read+write
+ * cursors. The buffer's size always changes from the end: shrinking it removes bytes from the
+ * end; growing it adds capacity to the end.
+ *
+ * Warnings
+ * --------
+ *
+ * Most application developers should avoid this API. Those that must use this API should
+ * respect these warnings.
+ *
+ * **Don't mutate a cursor.** This class has public, non-final fields because that is convenient
+ * for low-level I/O frameworks. Never assign values to these fields; instead use the cursor API
+ * to adjust these.
+ *
+ * **Never mutate `data` unless you have read+write access.** You are on the honor system to never
+ * write the buffer in read-only mode. Read-only mode may be more efficient than read+write mode
+ * because it does not need to make private copies of shared segments.
+ *
+ * **Only access data in `[start..end)`.** Other data in the byte array is undefined! It may
+ * contain private or sensitive data from other parts of your process.
+ *
+ * **Always fill the new capacity when you grow a buffer.** New capacity is not zero-filled and
+ * may contain data from other parts of your process. Avoid leaking this information by always
+ * writing something to the newly-allocated capacity. Do not assume that new capacity will be
+ * filled with `0`; it will not be.
+ *
+ * **Do not access a buffer while is being accessed by a cursor.** Even simple read-only
+ * operations like [Buffer.clone] are unsafe because they mark segments as shared.
+ *
+ * **Do not hard-code the segment size in your application.** It is possible that segment sizes
+ * will change with advances in hardware. Future versions of Okio may even have heterogeneous
+ * segment sizes.
+ *
+ * These warnings are intended to help you to use this API safely. It's here for developers
+ * that need absolutely the most throughput. Since that's you, here's one final performance tip.
+ * You can reuse instances of this class if you like. Use the overloads of [Buffer.readUnsafe] and
+ * [Buffer.readAndWriteUnsafe] that take a cursor and close it after use.
+ */
+ class UnsafeCursor constructor() {
+ @JvmField var buffer: Buffer?
+ @JvmField var readWrite: Boolean
+
+ internal var segment: Segment?
+ @JvmField var offset: Long
+ @JvmField var data: ByteArray?
+ @JvmField var start: Int
+ @JvmField var end: Int
+
+ /**
+ * Seeks to the next range of bytes, advancing the offset by `end - start`. Returns the size of
+ * the readable range (at least 1), or -1 if we have reached the end of the buffer and there are
+ * no more bytes to read.
+ */
+ fun next(): Int
+
+ /**
+ * Reposition the cursor so that the data at [offset] is readable at `data[start]`.
+ * Returns the number of bytes readable in [data] (at least 1), or -1 if there are no data
+ * to read.
+ */
+ fun seek(offset: Long): Int
+
+ /**
+ * Change the size of the buffer so that it equals [newSize] by either adding new capacity at
+ * the end or truncating the buffer at the end. Newly added capacity may span multiple segments.
+ *
+ * As a side-effect this cursor will [seek][UnsafeCursor.seek]. If the buffer is being enlarged
+ * it will move [UnsafeCursor.offset] to the first byte of newly-added capacity. This is the
+ * size of the buffer prior to the `resizeBuffer()` call. If the buffer is being shrunk it will move
+ * [UnsafeCursor.offset] to the end of the buffer.
+ *
+ * Warning: it is the caller’s responsibility to write new data to every byte of the
+ * newly-allocated capacity. Failure to do so may cause serious security problems as the data
+ * in the returned buffers is not zero filled. Buffers may contain dirty pooled segments that
+ * hold very sensitive data from other parts of the current process.
+ *
+ * @return the previous size of the buffer.
+ */
+ fun resizeBuffer(newSize: Long): Long
+
+ /**
+ * Grow the buffer by adding a **contiguous range** of capacity in a single segment. This adds
+ * at least [minByteCount] bytes but may add up to a full segment of additional capacity.
+ *
+ * As a side-effect this cursor will [seek][UnsafeCursor.seek]. It will move
+ * [offset][UnsafeCursor.offset] to the first byte of newly-added capacity. This is the size of
+ * the buffer prior to the `expandBuffer()` call.
+ *
+ * If [minByteCount] bytes are available in the buffer's current tail segment that will be used;
+ * otherwise another segment will be allocated and appended. In either case this returns the
+ * number of bytes of capacity added to this buffer.
+ *
+ * Warning: it is the caller’s responsibility to either write new data to every byte of the
+ * newly-allocated capacity, or to [shrink][UnsafeCursor.resizeBuffer] the buffer to the data
+ * written. Failure to do so may cause serious security problems as the data in the returned
+ * buffers is not zero filled. Buffers may contain dirty pooled segments that hold very
+ * sensitive data from other parts of the current process.
+ *
+ * @param minByteCount the size of the contiguous capacity. Must be positive and not greater
+ * than the capacity size of a single segment (8 KiB).
+ * @return the number of bytes expanded by. Not less than `minByteCount`.
+ */
+ fun expandBuffer(minByteCount: Int): Long
+
+ fun close()
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/BufferedSink.kt b/okio/src/commonMain/kotlin/okio/BufferedSink.kt
new file mode 100644
index 00000000..40c26585
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/BufferedSink.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * A sink that keeps a buffer internally so that callers can do small writes without a performance
+ * penalty.
+ */
+expect interface BufferedSink : Sink {
+ /** This sink's internal buffer. */
+ val buffer: Buffer
+
+ fun write(byteString: ByteString): BufferedSink
+
+ fun write(byteString: ByteString, offset: Int, byteCount: Int): BufferedSink
+
+ /** Like [OutputStream.write], this writes a complete byte array to this sink. */
+ fun write(source: ByteArray): BufferedSink
+
+ /** Like [OutputStream.write], this writes `byteCount` bytes of `source`, starting at `offset`. */
+ fun write(source: ByteArray, offset: Int, byteCount: Int): BufferedSink
+
+ /**
+ * Removes all bytes from `source` and appends them to this sink. Returns the number of bytes read
+ * which will be 0 if `source` is exhausted.
+ */
+ fun writeAll(source: Source): Long
+
+ /** Removes `byteCount` bytes from `source` and appends them to this sink. */
+ fun write(source: Source, byteCount: Long): BufferedSink
+
+ /**
+ * Encodes `string` in UTF-8 and writes it to this sink.
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeUtf8("Uh uh uh!");
+ * buffer.writeByte(' ');
+ * buffer.writeUtf8("You didn't say the magic word!");
+ *
+ * assertEquals("Uh uh uh! You didn't say the magic word!", buffer.readUtf8());
+ * ```
+ */
+ fun writeUtf8(string: String): BufferedSink
+
+ /**
+ * Encodes the characters at `beginIndex` up to `endIndex` from `string` in UTF-8 and writes it to
+ * this sink.
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeUtf8("I'm a hacker!\n", 6, 12);
+ * buffer.writeByte(' ');
+ * buffer.writeUtf8("That's what I said: you're a nerd.\n", 29, 33);
+ * buffer.writeByte(' ');
+ * buffer.writeUtf8("I prefer to be called a hacker!\n", 24, 31);
+ *
+ * assertEquals("hacker nerd hacker!", buffer.readUtf8());
+ * ```
+ */
+ fun writeUtf8(string: String, beginIndex: Int, endIndex: Int): BufferedSink
+
+ /** Encodes `codePoint` in UTF-8 and writes it to this sink. */
+ fun writeUtf8CodePoint(codePoint: Int): BufferedSink
+
+ /** Writes a byte to this sink. */
+ fun writeByte(b: Int): BufferedSink
+
+ /**
+ * Writes a big-endian short to this sink using two bytes.
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeShort(32767);
+ * buffer.writeShort(15);
+ *
+ * assertEquals(4, buffer.size());
+ * assertEquals((byte) 0x7f, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x0f, buffer.readByte());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun writeShort(s: Int): BufferedSink
+
+ /**
+ * Writes a little-endian short to this sink using two bytes.
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeShortLe(32767);
+ * buffer.writeShortLe(15);
+ *
+ * assertEquals(4, buffer.size());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0x7f, buffer.readByte());
+ * assertEquals((byte) 0x0f, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun writeShortLe(s: Int): BufferedSink
+
+ /**
+ * Writes a big-endian int to this sink using four bytes.
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeInt(2147483647);
+ * buffer.writeInt(15);
+ *
+ * assertEquals(8, buffer.size());
+ * assertEquals((byte) 0x7f, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x0f, buffer.readByte());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun writeInt(i: Int): BufferedSink
+
+ /**
+ * Writes a little-endian int to this sink using four bytes.
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeIntLe(2147483647);
+ * buffer.writeIntLe(15);
+ *
+ * assertEquals(8, buffer.size());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0x7f, buffer.readByte());
+ * assertEquals((byte) 0x0f, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun writeIntLe(i: Int): BufferedSink
+
+ /**
+ * Writes a big-endian long to this sink using eight bytes.
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeLong(9223372036854775807L);
+ * buffer.writeLong(15);
+ *
+ * assertEquals(16, buffer.size());
+ * assertEquals((byte) 0x7f, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x0f, buffer.readByte());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun writeLong(v: Long): BufferedSink
+
+ /**
+ * Writes a little-endian long to this sink using eight bytes.
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeLongLe(9223372036854775807L);
+ * buffer.writeLongLe(15);
+ *
+ * assertEquals(16, buffer.size());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0xff, buffer.readByte());
+ * assertEquals((byte) 0x7f, buffer.readByte());
+ * assertEquals((byte) 0x0f, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals((byte) 0x00, buffer.readByte());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun writeLongLe(v: Long): BufferedSink
+
+ /**
+ * Writes a long to this sink in signed decimal form (i.e., as a string in base 10).
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeDecimalLong(8675309L);
+ * buffer.writeByte(' ');
+ * buffer.writeDecimalLong(-123L);
+ * buffer.writeByte(' ');
+ * buffer.writeDecimalLong(1L);
+ *
+ * assertEquals("8675309 -123 1", buffer.readUtf8());
+ * ```
+ */
+ fun writeDecimalLong(v: Long): BufferedSink
+
+ /**
+ * Writes a long to this sink in hexadecimal form (i.e., as a string in base 16).
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeHexadecimalUnsignedLong(65535L);
+ * buffer.writeByte(' ');
+ * buffer.writeHexadecimalUnsignedLong(0xcafebabeL);
+ * buffer.writeByte(' ');
+ * buffer.writeHexadecimalUnsignedLong(0x10L);
+ *
+ * assertEquals("ffff cafebabe 10", buffer.readUtf8());
+ * ```
+ */
+ fun writeHexadecimalUnsignedLong(v: Long): BufferedSink
+
+ /**
+ * Writes all buffered data to the underlying sink, if one exists. Then that sink is recursively
+ * flushed which pushes data as far as possible towards its ultimate destination. Typically that
+ * destination is a network socket or file.
+ * ```
+ * BufferedSink b0 = new Buffer();
+ * BufferedSink b1 = Okio.buffer(b0);
+ * BufferedSink b2 = Okio.buffer(b1);
+ *
+ * b2.writeUtf8("hello");
+ * assertEquals(5, b2.buffer().size());
+ * assertEquals(0, b1.buffer().size());
+ * assertEquals(0, b0.buffer().size());
+ *
+ * b2.flush();
+ * assertEquals(0, b2.buffer().size());
+ * assertEquals(0, b1.buffer().size());
+ * assertEquals(5, b0.buffer().size());
+ * ```
+ */
+ override fun flush()
+
+ /**
+ * Writes all buffered data to the underlying sink, if one exists. Like [flush], but weaker. Call
+ * this before this buffered sink goes out of scope so that its data can reach its destination.
+ * ```
+ * BufferedSink b0 = new Buffer();
+ * BufferedSink b1 = Okio.buffer(b0);
+ * BufferedSink b2 = Okio.buffer(b1);
+ *
+ * b2.writeUtf8("hello");
+ * assertEquals(5, b2.buffer().size());
+ * assertEquals(0, b1.buffer().size());
+ * assertEquals(0, b0.buffer().size());
+ *
+ * b2.emit();
+ * assertEquals(0, b2.buffer().size());
+ * assertEquals(5, b1.buffer().size());
+ * assertEquals(0, b0.buffer().size());
+ *
+ * b1.emit();
+ * assertEquals(0, b2.buffer().size());
+ * assertEquals(0, b1.buffer().size());
+ * assertEquals(5, b0.buffer().size());
+ * ```
+ */
+ fun emit(): BufferedSink
+
+ /**
+ * Writes complete segments to the underlying sink, if one exists. Like [flush], but weaker. Use
+ * this to limit the memory held in the buffer to a single segment. Typically application code
+ * will not need to call this: it is only necessary when application code writes directly to this
+ * [sink's buffer][buffer].
+ * ```
+ * BufferedSink b0 = new Buffer();
+ * BufferedSink b1 = Okio.buffer(b0);
+ * BufferedSink b2 = Okio.buffer(b1);
+ *
+ * b2.buffer().write(new byte[20_000]);
+ * assertEquals(20_000, b2.buffer().size());
+ * assertEquals( 0, b1.buffer().size());
+ * assertEquals( 0, b0.buffer().size());
+ *
+ * b2.emitCompleteSegments();
+ * assertEquals( 3_616, b2.buffer().size());
+ * assertEquals( 0, b1.buffer().size());
+ * assertEquals(16_384, b0.buffer().size()); // This example assumes 8192 byte segments.
+ * ```
+ */
+ fun emitCompleteSegments(): BufferedSink
+}
diff --git a/okio/src/commonMain/kotlin/okio/BufferedSource.kt b/okio/src/commonMain/kotlin/okio/BufferedSource.kt
new file mode 100644
index 00000000..0ba4d152
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/BufferedSource.kt
@@ -0,0 +1,534 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * A source that keeps a buffer internally so that callers can do small reads without a performance
+ * penalty. It also allows clients to read ahead, buffering as much as necessary before consuming
+ * input.
+ */
+expect interface BufferedSource : Source {
+ /** This source's internal buffer. */
+ val buffer: Buffer
+
+ /**
+ * Returns true if there are no more bytes in this source. This will block until there are bytes
+ * to read or the source is definitely exhausted.
+ */
+ fun exhausted(): Boolean
+
+ /**
+ * Returns when the buffer contains at least `byteCount` bytes. Throws an
+ * [java.io.EOFException] if the source is exhausted before the required bytes can be read.
+ */
+ fun require(byteCount: Long)
+
+ /**
+ * Returns true when the buffer contains at least `byteCount` bytes, expanding it as
+ * necessary. Returns false if the source is exhausted before the requested bytes can be read.
+ */
+ fun request(byteCount: Long): Boolean
+
+ /** Removes a byte from this source and returns it. */
+ fun readByte(): Byte
+
+ /**
+ * Removes two bytes from this source and returns a big-endian short.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeByte(0x7f)
+ * .writeByte(0xff)
+ * .writeByte(0x00)
+ * .writeByte(0x0f);
+ * assertEquals(4, buffer.size());
+ *
+ * assertEquals(32767, buffer.readShort());
+ * assertEquals(2, buffer.size());
+ *
+ * assertEquals(15, buffer.readShort());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun readShort(): Short
+
+ /**
+ * Removes two bytes from this source and returns a little-endian short.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeByte(0xff)
+ * .writeByte(0x7f)
+ * .writeByte(0x0f)
+ * .writeByte(0x00);
+ * assertEquals(4, buffer.size());
+ *
+ * assertEquals(32767, buffer.readShortLe());
+ * assertEquals(2, buffer.size());
+ *
+ * assertEquals(15, buffer.readShortLe());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun readShortLe(): Short
+
+ /**
+ * Removes four bytes from this source and returns a big-endian int.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeByte(0x7f)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x0f);
+ * assertEquals(8, buffer.size());
+ *
+ * assertEquals(2147483647, buffer.readInt());
+ * assertEquals(4, buffer.size());
+ *
+ * assertEquals(15, buffer.readInt());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun readInt(): Int
+
+ /**
+ * Removes four bytes from this source and returns a little-endian int.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0x7f)
+ * .writeByte(0x0f)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00);
+ * assertEquals(8, buffer.size());
+ *
+ * assertEquals(2147483647, buffer.readIntLe());
+ * assertEquals(4, buffer.size());
+ *
+ * assertEquals(15, buffer.readIntLe());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun readIntLe(): Int
+
+ /**
+ * Removes eight bytes from this source and returns a big-endian long.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeByte(0x7f)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x0f);
+ * assertEquals(16, buffer.size());
+ *
+ * assertEquals(9223372036854775807L, buffer.readLong());
+ * assertEquals(8, buffer.size());
+ *
+ * assertEquals(15, buffer.readLong());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun readLong(): Long
+
+ /**
+ * Removes eight bytes from this source and returns a little-endian long.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0xff)
+ * .writeByte(0x7f)
+ * .writeByte(0x0f)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00)
+ * .writeByte(0x00);
+ * assertEquals(16, buffer.size());
+ *
+ * assertEquals(9223372036854775807L, buffer.readLongLe());
+ * assertEquals(8, buffer.size());
+ *
+ * assertEquals(15, buffer.readLongLe());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun readLongLe(): Long
+
+ /**
+ * Reads a long from this source in signed decimal form (i.e., as a string in base 10 with
+ * optional leading '-'). This will iterate until a non-digit character is found.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeUtf8("8675309 -123 00001");
+ *
+ * assertEquals(8675309L, buffer.readDecimalLong());
+ * assertEquals(' ', buffer.readByte());
+ * assertEquals(-123L, buffer.readDecimalLong());
+ * assertEquals(' ', buffer.readByte());
+ * assertEquals(1L, buffer.readDecimalLong());
+ * ```
+ *
+ * @throws NumberFormatException if the found digits do not fit into a `long` or a decimal
+ * number was not present.
+ */
+ fun readDecimalLong(): Long
+
+ /**
+ * Reads a long form this source in hexadecimal form (i.e., as a string in base 16). This will
+ * iterate until a non-hexadecimal character is found.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeUtf8("ffff CAFEBABE 10");
+ *
+ * assertEquals(65535L, buffer.readHexadecimalUnsignedLong());
+ * assertEquals(' ', buffer.readByte());
+ * assertEquals(0xcafebabeL, buffer.readHexadecimalUnsignedLong());
+ * assertEquals(' ', buffer.readByte());
+ * assertEquals(0x10L, buffer.readHexadecimalUnsignedLong());
+ * ```
+ *
+ * @throws NumberFormatException if the found hexadecimal does not fit into a `long` or
+ * hexadecimal was not found.
+ */
+ fun readHexadecimalUnsignedLong(): Long
+
+ /**
+ * Reads and discards `byteCount` bytes from this source. Throws an [java.io.EOFException] if the
+ * source is exhausted before the requested bytes can be skipped.
+ */
+ fun skip(byteCount: Long)
+
+ /** Removes all bytes from this and returns them as a byte string. */
+ fun readByteString(): ByteString
+
+ /** Removes `byteCount` bytes from this and returns them as a byte string. */
+ fun readByteString(byteCount: Long): ByteString
+
+ /**
+ * Finds the first string in `options` that is a prefix of this buffer, consumes it from this
+ * buffer, and returns its index. If no byte string in `options` is a prefix of this buffer this
+ * returns -1 and no bytes are consumed.
+ *
+ * This can be used as an alternative to [readByteString] or even [readUtf8] if the set of
+ * expected values is known in advance.
+ * ```
+ * Options FIELDS = Options.of(
+ * ByteString.encodeUtf8("depth="),
+ * ByteString.encodeUtf8("height="),
+ * ByteString.encodeUtf8("width="));
+ *
+ * Buffer buffer = new Buffer()
+ * .writeUtf8("width=640\n")
+ * .writeUtf8("height=480\n");
+ *
+ * assertEquals(2, buffer.select(FIELDS));
+ * assertEquals(640, buffer.readDecimalLong());
+ * assertEquals('\n', buffer.readByte());
+ * assertEquals(1, buffer.select(FIELDS));
+ * assertEquals(480, buffer.readDecimalLong());
+ * assertEquals('\n', buffer.readByte());
+ * ```
+ */
+ fun select(options: Options): Int
+
+ /** Removes all bytes from this and returns them as a byte array. */
+ fun readByteArray(): ByteArray
+
+ /** Removes `byteCount` bytes from this and returns them as a byte array. */
+ fun readByteArray(byteCount: Long): ByteArray
+
+ /**
+ * Removes up to `sink.length` bytes from this and copies them into `sink`. Returns the number of
+ * bytes read, or -1 if this source is exhausted.
+ */
+ fun read(sink: ByteArray): Int
+
+ /**
+ * Removes exactly `sink.length` bytes from this and copies them into `sink`. Throws an
+ * [java.io.EOFException] if the requested number of bytes cannot be read.
+ */
+ fun readFully(sink: ByteArray)
+
+ /**
+ * Removes up to `byteCount` bytes from this and copies them into `sink` at `offset`. Returns the
+ * number of bytes read, or -1 if this source is exhausted.
+ */
+ fun read(sink: ByteArray, offset: Int, byteCount: Int): Int
+
+ /**
+ * Removes exactly `byteCount` bytes from this and appends them to `sink`. Throws an
+ * [java.io.EOFException] if the requested number of bytes cannot be read.
+ */
+ fun readFully(sink: Buffer, byteCount: Long)
+
+ /**
+ * Removes all bytes from this and appends them to `sink`. Returns the total number of bytes
+ * written to `sink` which will be 0 if this is exhausted.
+ */
+ fun readAll(sink: Sink): Long
+
+ /**
+ * Removes all bytes from this, decodes them as UTF-8, and returns the string. Returns the empty
+ * string if this source is empty.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeUtf8("Uh uh uh!")
+ * .writeByte(' ')
+ * .writeUtf8("You didn't say the magic word!");
+ *
+ * assertEquals("Uh uh uh! You didn't say the magic word!", buffer.readUtf8());
+ * assertEquals(0, buffer.size());
+ *
+ * assertEquals("", buffer.readUtf8());
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun readUtf8(): String
+
+ /**
+ * Removes `byteCount` bytes from this, decodes them as UTF-8, and returns the string.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeUtf8("Uh uh uh!")
+ * .writeByte(' ')
+ * .writeUtf8("You didn't say the magic word!");
+ * assertEquals(40, buffer.size());
+ *
+ * assertEquals("Uh uh uh! You ", buffer.readUtf8(14));
+ * assertEquals(26, buffer.size());
+ *
+ * assertEquals("didn't say the", buffer.readUtf8(14));
+ * assertEquals(12, buffer.size());
+ *
+ * assertEquals(" magic word!", buffer.readUtf8(12));
+ * assertEquals(0, buffer.size());
+ * ```
+ */
+ fun readUtf8(byteCount: Long): String
+
+ /**
+ * Removes and returns characters up to but not including the next line break. A line break is
+ * either `"\n"` or `"\r\n"`; these characters are not included in the result.
+ * ```
+ * Buffer buffer = new Buffer()
+ * .writeUtf8("I'm a hacker!\n")
+ * .writeUtf8("That's what I said: you're a nerd.\n")
+ * .writeUtf8("I prefer to be called a hacker!\n");
+ * assertEquals(81, buffer.size());
+ *
+ * assertEquals("I'm a hacker!", buffer.readUtf8Line());
+ * assertEquals(67, buffer.size());
+ *
+ * assertEquals("That's what I said: you're a nerd.", buffer.readUtf8Line());
+ * assertEquals(32, buffer.size());
+ *
+ * assertEquals("I prefer to be called a hacker!", buffer.readUtf8Line());
+ * assertEquals(0, buffer.size());
+ *
+ * assertEquals(null, buffer.readUtf8Line());
+ * assertEquals(0, buffer.size());
+ * ```
+ *
+ * **On the end of the stream this method returns null,** just like [java.io.BufferedReader]. If
+ * the source doesn't end with a line break then an implicit line break is assumed. Null is
+ * returned once the source is exhausted. Use this for human-generated data, where a trailing
+ * line break is optional.
+ */
+ fun readUtf8Line(): String?
+
+ /**
+ * Removes and returns characters up to but not including the next line break. A line break is
+ * either `"\n"` or `"\r\n"`; these characters are not included in the result.
+ *
+ * **On the end of the stream this method throws.** Every call must consume either
+ * '\r\n' or '\n'. If these characters are absent in the stream, an [java.io.EOFException]
+ * is thrown. Use this for machine-generated data where a missing line break implies truncated
+ * input.
+ */
+ fun readUtf8LineStrict(): String
+
+ /**
+ * Like [readUtf8LineStrict], except this allows the caller to specify the longest allowed match.
+ * Use this to protect against streams that may not include `"\n"` or `"\r\n"`.
+ *
+ * The returned string will have at most `limit` UTF-8 bytes, and the maximum number of bytes
+ * scanned is `limit + 2`. If `limit == 0` this will always throw an `EOFException` because no
+ * bytes will be scanned.
+ *
+ * This method is safe. No bytes are discarded if the match fails, and the caller is free to try
+ * another match:
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeUtf8("12345\r\n");
+ *
+ * // This will throw! There must be \r\n or \n at the limit or before it.
+ * buffer.readUtf8LineStrict(4);
+ *
+ * // No bytes have been consumed so the caller can retry.
+ * assertEquals("12345", buffer.readUtf8LineStrict(5));
+ * ```
+ */
+ fun readUtf8LineStrict(limit: Long): String
+
+ /**
+ * Removes and returns a single UTF-8 code point, reading between 1 and 4 bytes as necessary.
+ *
+ * If this source is exhausted before a complete code point can be read, this throws an
+ * [java.io.EOFException] and consumes no input.
+ *
+ * If this source doesn't start with a properly-encoded UTF-8 code point, this method will remove
+ * 1 or more non-UTF-8 bytes and return the replacement character (`U+FFFD`). This covers encoding
+ * problems (the input is not properly-encoded UTF-8), characters out of range (beyond the
+ * 0x10ffff limit of Unicode), code points for UTF-16 surrogates (U+d800..U+dfff) and overlong
+ * encodings (such as `0xc080` for the NUL character in modified UTF-8).
+ */
+ fun readUtf8CodePoint(): Int
+
+ /** Equivalent to [indexOf(b, 0)][indexOf]. */
+ fun indexOf(b: Byte): Long
+
+ /**
+ * Returns the index of the first `b` in the buffer at or after `fromIndex`. This expands the
+ * buffer as necessary until `b` is found. This reads an unbounded number of bytes into the
+ * buffer. Returns -1 if the stream is exhausted before the requested byte is found.
+ * ```
+ * Buffer buffer = new Buffer();
+ * buffer.writeUtf8("Don't move! He can't see us if we don't move.");
+ *
+ * byte m = 'm';
+ * assertEquals(6, buffer.indexOf(m));
+ * assertEquals(40, buffer.indexOf(m, 12));
+ * ```
+ */
+ fun indexOf(b: Byte, fromIndex: Long): Long
+
+ /**
+ * Returns the index of `b` if it is found in the range of `fromIndex` inclusive to `toIndex`
+ * exclusive. If `b` isn't found, or if `fromIndex == toIndex`, then -1 is returned.
+ *
+ * The scan terminates at either `toIndex` or the end of the buffer, whichever comes first. The
+ * maximum number of bytes scanned is `toIndex-fromIndex`.
+ */
+ fun indexOf(b: Byte, fromIndex: Long, toIndex: Long): Long
+
+ /** Equivalent to [indexOf(bytes, 0)][indexOf]. */
+ fun indexOf(bytes: ByteString): Long
+
+ /**
+ * Returns the index of the first match for `bytes` in the buffer at or after `fromIndex`. This
+ * expands the buffer as necessary until `bytes` is found. This reads an unbounded number of
+ * bytes into the buffer. Returns -1 if the stream is exhausted before the requested bytes are
+ * found.
+ * ```
+ * ByteString MOVE = ByteString.encodeUtf8("move");
+ *
+ * Buffer buffer = new Buffer();
+ * buffer.writeUtf8("Don't move! He can't see us if we don't move.");
+ *
+ * assertEquals(6, buffer.indexOf(MOVE));
+ * assertEquals(40, buffer.indexOf(MOVE, 12));
+ * ```
+ */
+ fun indexOf(bytes: ByteString, fromIndex: Long): Long
+
+ /** Equivalent to [indexOfElement(targetBytes, 0)][indexOfElement]. */
+ fun indexOfElement(targetBytes: ByteString): Long
+
+ /**
+ * Returns the first index in this buffer that is at or after `fromIndex` and that contains any of
+ * the bytes in `targetBytes`. This expands the buffer as necessary until a target byte is found.
+ * This reads an unbounded number of bytes into the buffer. Returns -1 if the stream is exhausted
+ * before the requested byte is found.
+ * ```
+ * ByteString ANY_VOWEL = ByteString.encodeUtf8("AEOIUaeoiu");
+ *
+ * Buffer buffer = new Buffer();
+ * buffer.writeUtf8("Dr. Alan Grant");
+ *
+ * assertEquals(4, buffer.indexOfElement(ANY_VOWEL)); // 'A' in 'Alan'.
+ * assertEquals(11, buffer.indexOfElement(ANY_VOWEL, 9)); // 'a' in 'Grant'.
+ * ```
+ */
+ fun indexOfElement(targetBytes: ByteString, fromIndex: Long): Long
+
+ /**
+ * Returns true if the bytes at `offset` in this source equal `bytes`. This expands the buffer as
+ * necessary until a byte does not match, all bytes are matched, or if the stream is exhausted
+ * before enough bytes could determine a match.
+ * ```
+ * ByteString simonSays = ByteString.encodeUtf8("Simon says:");
+ *
+ * Buffer standOnOneLeg = new Buffer().writeUtf8("Simon says: Stand on one leg.");
+ * assertTrue(standOnOneLeg.rangeEquals(0, simonSays));
+ *
+ * Buffer payMeMoney = new Buffer().writeUtf8("Pay me $1,000,000.");
+ * assertFalse(payMeMoney.rangeEquals(0, simonSays));
+ * ```
+ */
+ fun rangeEquals(offset: Long, bytes: ByteString): Boolean
+
+ /**
+ * Returns true if `byteCount` bytes at `offset` in this source equal `bytes` at `bytesOffset`.
+ * This expands the buffer as necessary until a byte does not match, all bytes are matched, or if
+ * the stream is exhausted before enough bytes could determine a match.
+ */
+ fun rangeEquals(offset: Long, bytes: ByteString, bytesOffset: Int, byteCount: Int): Boolean
+
+ /**
+ * Returns a new `BufferedSource` that can read data from this `BufferedSource` without consuming
+ * it. The returned source becomes invalid once this source is next read or closed.
+ *
+ * For example, we can use `peek()` to lookahead and read the same data multiple times.
+ *
+ * ```
+ * val buffer = Buffer()
+ * buffer.writeUtf8("abcdefghi")
+ *
+ * buffer.readUtf8(3) // returns "abc", buffer contains "defghi"
+ *
+ * val peek = buffer.peek()
+ * peek.readUtf8(3) // returns "def", buffer contains "defghi"
+ * peek.readUtf8(3) // returns "ghi", buffer contains "defghi"
+ *
+ * buffer.readUtf8(3) // returns "def", buffer contains "ghi"
+ * ```
+ */
+ fun peek(): BufferedSource
+}
diff --git a/okio/src/commonMain/kotlin/okio/ByteString.kt b/okio/src/commonMain/kotlin/okio/ByteString.kt
new file mode 100644
index 00000000..7eb34c6a
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/ByteString.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmStatic
+
+/**
+ * An immutable sequence of bytes.
+ *
+ * Byte strings compare lexicographically as a sequence of **unsigned** bytes. That is, the byte
+ * string `ff` sorts after `00`. This is counter to the sort order of the corresponding bytes,
+ * where `-1` sorts before `0`.
+ *
+ * **Full disclosure:** this class provides untrusted input and output streams with raw access to
+ * the underlying byte array. A hostile stream implementation could keep a reference to the mutable
+ * byte string, violating the immutable guarantee of this class. For this reason a byte string's
+ * immutability guarantee cannot be relied upon for security in applets and other environments that
+ * run both trusted and untrusted code in the same process.
+ */
+expect open class ByteString
+// Trusted internal constructor doesn't clone data.
+internal constructor(data: ByteArray) : Comparable<ByteString> {
+ internal val data: ByteArray
+
+ internal var hashCode: Int
+ internal var utf8: String?
+
+ /** Constructs a new `String` by decoding the bytes as `UTF-8`. */
+ fun utf8(): String
+
+ /**
+ * Returns this byte string encoded as [Base64](http://www.ietf.org/rfc/rfc2045.txt). In violation
+ * of the RFC, the returned string does not wrap lines at 76 columns.
+ */
+ fun base64(): String
+
+ /** Returns this byte string encoded as [URL-safe Base64](http://www.ietf.org/rfc/rfc4648.txt). */
+ fun base64Url(): String
+
+ /** Returns this byte string encoded in hexadecimal. */
+ fun hex(): String
+
+ /** Returns the 128-bit MD5 hash of this byte string. */
+ fun md5(): ByteString
+
+ /** Returns the 160-bit SHA-1 hash of this byte string. */
+ fun sha1(): ByteString
+
+ /** Returns the 256-bit SHA-256 hash of this byte string. */
+ fun sha256(): ByteString
+
+ /** Returns the 512-bit SHA-512 hash of this byte string. */
+ fun sha512(): ByteString
+
+ /** Returns the 160-bit SHA-1 HMAC of this byte string. */
+ fun hmacSha1(key: ByteString): ByteString
+
+ /** Returns the 256-bit SHA-256 HMAC of this byte string. */
+ fun hmacSha256(key: ByteString): ByteString
+
+ /** Returns the 512-bit SHA-512 HMAC of this byte string. */
+ fun hmacSha512(key: ByteString): ByteString
+ /**
+ * Returns a byte string equal to this byte string, but with the bytes 'A' through 'Z' replaced
+ * with the corresponding byte in 'a' through 'z'. Returns this byte string if it contains no
+ * bytes in 'A' through 'Z'.
+ */
+ fun toAsciiLowercase(): ByteString
+
+ /**
+ * Returns a byte string that is a substring of this byte string, beginning at the specified
+ * `beginIndex` and ends at the specified `endIndex`. Returns this byte string if `beginIndex` is
+ * 0 and `endIndex` is the length of this byte string.
+ */
+ fun substring(beginIndex: Int = 0, endIndex: Int = size): ByteString
+
+ /**
+ * Returns a byte string equal to this byte string, but with the bytes 'a' through 'z' replaced
+ * with the corresponding byte in 'A' through 'Z'. Returns this byte string if it contains no
+ * bytes in 'a' through 'z'.
+ */
+ fun toAsciiUppercase(): ByteString
+
+ /** Returns the byte at `pos`. */
+ internal fun internalGet(pos: Int): Byte
+
+ /** Returns the byte at `index`. */
+ @JvmName("getByte")
+ operator fun get(index: Int): Byte
+
+ /** Returns the number of bytes in this ByteString. */
+ val size: Int
+ @JvmName("size") get
+
+ // Hack to work around Kotlin's limitation for using JvmName on open/override vals/funs
+ internal fun getSize(): Int
+
+ /** Returns a byte array containing a copy of the bytes in this `ByteString`. */
+ fun toByteArray(): ByteArray
+
+ /** Writes the contents of this byte string to `buffer`. */
+ internal fun write(buffer: Buffer, offset: Int, byteCount: Int)
+
+ /** Returns the bytes of this string without a defensive copy. Do not mutate! */
+ internal fun internalArray(): ByteArray
+
+ /**
+ * Returns true if the bytes of this in `[offset..offset+byteCount)` equal the bytes of `other` in
+ * `[otherOffset..otherOffset+byteCount)`. Returns false if either range is out of bounds.
+ */
+ fun rangeEquals(offset: Int, other: ByteString, otherOffset: Int, byteCount: Int): Boolean
+
+ /**
+ * Returns true if the bytes of this in `[offset..offset+byteCount)` equal the bytes of `other` in
+ * `[otherOffset..otherOffset+byteCount)`. Returns false if either range is out of bounds.
+ */
+ fun rangeEquals(offset: Int, other: ByteArray, otherOffset: Int, byteCount: Int): Boolean
+
+ fun startsWith(prefix: ByteString): Boolean
+
+ fun startsWith(prefix: ByteArray): Boolean
+
+ fun endsWith(suffix: ByteString): Boolean
+
+ fun endsWith(suffix: ByteArray): Boolean
+
+ @JvmOverloads
+ fun indexOf(other: ByteString, fromIndex: Int = 0): Int
+
+ @JvmOverloads
+ fun indexOf(other: ByteArray, fromIndex: Int = 0): Int
+
+ fun lastIndexOf(other: ByteString, fromIndex: Int = size): Int
+
+ fun lastIndexOf(other: ByteArray, fromIndex: Int = size): Int
+
+ override fun equals(other: Any?): Boolean
+
+ override fun hashCode(): Int
+
+ override fun compareTo(other: ByteString): Int
+
+ /**
+ * Returns a human-readable string that describes the contents of this byte string. Typically this
+ * is a string like `[text=Hello]` or `[hex=0000ffff]`.
+ */
+ override fun toString(): String
+
+ companion object {
+ /** A singleton empty `ByteString`. */
+ @JvmField
+ val EMPTY: ByteString
+
+ /** Returns a new byte string containing a clone of the bytes of `data`. */
+ @JvmStatic
+ fun of(vararg data: Byte): ByteString
+
+ /**
+ * Returns a new [ByteString] containing a copy of `byteCount` bytes of this [ByteArray]
+ * starting at `offset`.
+ */
+ @JvmStatic
+ fun ByteArray.toByteString(offset: Int = 0, byteCount: Int = size): ByteString
+
+ /** Returns a new byte string containing the `UTF-8` bytes of this [String]. */
+ @JvmStatic
+ fun String.encodeUtf8(): ByteString
+
+ /**
+ * Decodes the Base64-encoded bytes and returns their value as a byte string. Returns null if
+ * this is not a Base64-encoded sequence of bytes.
+ */
+ @JvmStatic
+ fun String.decodeBase64(): ByteString?
+
+ /** Decodes the hex-encoded bytes and returns their value a byte string. */
+ @JvmStatic
+ fun String.decodeHex(): ByteString
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/ExperimentalFileSystem.kt b/okio/src/commonMain/kotlin/okio/ExperimentalFileSystem.kt
new file mode 100644
index 00000000..7fc3a4a2
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/ExperimentalFileSystem.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlin.RequiresOptIn.Level.ERROR
+import kotlin.annotation.AnnotationRetention.BINARY
+import kotlin.annotation.AnnotationTarget.CLASS
+import kotlin.annotation.AnnotationTarget.FUNCTION
+import kotlin.annotation.AnnotationTarget.PROPERTY
+
+@RequiresOptIn(level = ERROR, message = "okio's FileSystem is unstable and subject to change")
+@Retention(BINARY)
+@Target(CLASS, FUNCTION, PROPERTY)
+annotation class ExperimentalFileSystem
diff --git a/okio/src/commonMain/kotlin/okio/HashingSink.kt b/okio/src/commonMain/kotlin/okio/HashingSink.kt
new file mode 100644
index 00000000..06cb6bf0
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/HashingSink.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * A sink that computes a hash of the full stream of bytes it has accepted. To use, create an
+ * instance with your preferred hash algorithm. Write all of the data to the sink and then call
+ * [hash] to compute the final hash value.
+ *
+ * In this example we use `HashingSink` with a [BufferedSink] to make writing to the
+ * sink easier.
+ * ```
+ * HashingSink hashingSink = HashingSink.sha256(s);
+ * BufferedSink bufferedSink = Okio.buffer(hashingSink);
+ *
+ * ... // Write to bufferedSink and either flush or close it.
+ *
+ * ByteString hash = hashingSink.hash();
+ * ```
+ */
+expect class HashingSink : Sink {
+
+ /**
+ * Returns the hash of the bytes accepted thus far and resets the internal state of this sink.
+ *
+ * **Warning:** This method is not idempotent. Each time this method is called its
+ * internal state is cleared. This starts a new hash with zero bytes accepted.
+ */
+ val hash: ByteString
+
+ companion object {
+ /** Returns a sink that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */
+ fun md5(sink: Sink): HashingSink
+
+ /** Returns a sink that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */
+ fun sha1(sink: Sink): HashingSink
+
+ /** Returns a sink that uses the SHA-256 hash algorithm to produce 256-bit hashes. */
+ fun sha256(sink: Sink): HashingSink
+
+ /** Returns a sink that uses the SHA-512 hash algorithm to produce 512-bit hashes. */
+ fun sha512(sink: Sink): HashingSink
+
+ /** Returns a sink that uses the obsolete SHA-1 HMAC algorithm to produce 160-bit hashes. */
+ fun hmacSha1(sink: Sink, key: ByteString): HashingSink
+
+ /** Returns a sink that uses the SHA-256 HMAC algorithm to produce 256-bit hashes. */
+ fun hmacSha256(sink: Sink, key: ByteString): HashingSink
+
+ /** Returns a sink that uses the SHA-512 HMAC algorithm to produce 512-bit hashes. */
+ fun hmacSha512(sink: Sink, key: ByteString): HashingSink
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/HashingSource.kt b/okio/src/commonMain/kotlin/okio/HashingSource.kt
new file mode 100644
index 00000000..9c61c995
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/HashingSource.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * A source that computes a hash of the full stream of bytes it has supplied. To use, create an
+ * instance with your preferred hash algorithm. Exhaust the source by reading all of its bytes and
+ * then call [hash] to compute the final hash value.
+ *
+ *
+ * In this example we use `HashingSource` with a [BufferedSource] to make reading
+ * from the source easier.
+ * ```
+ * HashingSource hashingSource = HashingSource.sha256(rawSource);
+ * BufferedSource bufferedSource = Okio.buffer(hashingSource);
+ *
+ * ... // Read all of bufferedSource.
+ *
+ * ByteString hash = hashingSource.hash();
+ * ```
+ */
+expect class HashingSource : Source {
+
+ /**
+ * Returns the hash of the bytes supplied thus far and resets the internal state of this source.
+ *
+ * **Warning:** This method is not idempotent. Each time this method is called its
+ * internal state is cleared. This starts a new hash with zero bytes supplied.
+ */
+ val hash: ByteString
+
+ companion object {
+ /** Returns a source that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */
+ fun md5(source: Source): HashingSource
+
+ /** Returns a source that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */
+ fun sha1(source: Source): HashingSource
+
+ /** Returns a source that uses the SHA-256 hash algorithm to produce 256-bit hashes. */
+ fun sha256(source: Source): HashingSource
+
+ /** Returns a source that uses the SHA-512 hash algorithm to produce 512-bit hashes. */
+ fun sha512(source: Source): HashingSource
+
+ /** Returns a source that uses the obsolete SHA-1 HMAC algorithm to produce 160-bit hashes. */
+ fun hmacSha1(source: Source, key: ByteString): HashingSource
+
+ /** Returns a source that uses the SHA-256 HMAC algorithm to produce 256-bit hashes. */
+ fun hmacSha256(source: Source, key: ByteString): HashingSource
+
+ /** Returns a source that uses the SHA-512 HMAC algorithm to produce 512-bit hashes. */
+ fun hmacSha512(source: Source, key: ByteString): HashingSource
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/Okio.kt b/okio/src/commonMain/kotlin/okio/Okio.kt
new file mode 100644
index 00000000..116678aa
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/Okio.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Essential APIs for working with Okio. */
+@file:JvmMultifileClass
+@file:JvmName("Okio")
+
+package okio
+
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
+/**
+ * Returns a new source that buffers reads from `source`. The returned source will perform bulk
+ * reads into its in-memory buffer. Use this wherever you read a source to get an ergonomic and
+ * efficient access to data.
+ */
+fun Source.buffer(): BufferedSource = RealBufferedSource(this)
+
+/**
+ * Returns a new sink that buffers writes to `sink`. The returned sink will batch writes to `sink`.
+ * Use this wherever you write to a sink to get an ergonomic and efficient access to data.
+ */
+fun Sink.buffer(): BufferedSink = RealBufferedSink(this)
+
+/** Returns a sink that writes nowhere. */
+@JvmName("blackhole")
+fun blackholeSink(): Sink = BlackholeSink()
+
+private class BlackholeSink : Sink {
+ override fun write(source: Buffer, byteCount: Long) = source.skip(byteCount)
+ override fun flush() {}
+ override fun timeout() = Timeout.NONE
+ override fun close() {}
+}
+
+/** Execute [block] then close this. This will be closed even if [block] throws. */
+inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
+ var result: R? = null
+ var thrown: Throwable? = null
+
+ try {
+ result = block(this)
+ } catch (t: Throwable) {
+ thrown = t
+ }
+
+ try {
+ this?.close()
+ } catch (t: Throwable) {
+ if (thrown == null) thrown = t
+ else thrown.addSuppressed(t)
+ }
+
+ if (thrown != null) throw thrown
+ return result!!
+}
diff --git a/okio/src/commonMain/kotlin/okio/Options.kt b/okio/src/commonMain/kotlin/okio/Options.kt
new file mode 100644
index 00000000..9ce07391
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/Options.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlin.jvm.JvmStatic
+
+/** An indexed set of values that may be read with [BufferedSource.select]. */
+class Options private constructor(
+ internal val byteStrings: Array<out ByteString>,
+ internal val trie: IntArray
+) : AbstractList<ByteString>(), RandomAccess {
+
+ override val size: Int
+ get() = byteStrings.size
+
+ override fun get(index: Int) = byteStrings[index]
+
+ companion object {
+ @JvmStatic
+ fun of(vararg byteStrings: ByteString): Options {
+ if (byteStrings.isEmpty()) {
+ // With no choices we must always return -1. Create a trie that selects from an empty set.
+ return Options(arrayOf(), intArrayOf(0, -1))
+ }
+
+ // Sort the byte strings which is required when recursively building the trie. Map the sorted
+ // indexes to the caller's indexes.
+ val list = byteStrings.toMutableList()
+ list.sort()
+ val indexes = mutableListOf(*byteStrings.map { -1 }.toTypedArray())
+ byteStrings.forEachIndexed { callerIndex, byteString ->
+ val sortedIndex = list.binarySearch(byteString)
+ indexes[sortedIndex] = callerIndex
+ }
+ require(list[0].size > 0) { "the empty byte string is not a supported option" }
+
+ // Strip elements that will never be returned because they follow their own prefixes. For
+ // example, if the caller provides ["abc", "abcde"] we will never return "abcde" because we
+ // return as soon as we encounter "abc".
+ var a = 0
+ while (a < list.size) {
+ val prefix = list[a]
+ var b = a + 1
+ while (b < list.size) {
+ val byteString = list[b]
+ if (!byteString.startsWith(prefix)) break
+ require(byteString.size != prefix.size) { "duplicate option: $byteString" }
+ if (indexes[b] > indexes[a]) {
+ list.removeAt(b)
+ indexes.removeAt(b)
+ } else {
+ b++
+ }
+ }
+ a++
+ }
+
+ val trieBytes = Buffer()
+ buildTrieRecursive(node = trieBytes, byteStrings = list, indexes = indexes)
+
+ val trie = IntArray(trieBytes.intCount.toInt())
+ var i = 0
+ while (!trieBytes.exhausted()) {
+ trie[i++] = trieBytes.readInt()
+ }
+
+ return Options(byteStrings.copyOf() /* Defensive copy. */, trie)
+ }
+
+ /**
+ * Builds a trie encoded as an int array. Nodes in the trie are of two types: SELECT and SCAN.
+ *
+ * SELECT nodes are encoded as:
+ * - selectChoiceCount: the number of bytes to choose between (a positive int)
+ * - prefixIndex: the result index at the current position or -1 if the current position is not
+ * a result on its own
+ * - a sorted list of selectChoiceCount bytes to match against the input string
+ * - a heterogeneous list of selectChoiceCount result indexes (>= 0) or offsets (< 0) of the
+ * next node to follow. Elements in this list correspond to elements in the preceding list.
+ * Offsets are negative and must be multiplied by -1 before being used.
+ *
+ * SCAN nodes are encoded as:
+ * - scanByteCount: the number of bytes to match in sequence. This count is negative and must
+ * be multiplied by -1 before being used.
+ * - prefixIndex: the result index at the current position or -1 if the current position is not
+ * a result on its own
+ * - a list of scanByteCount bytes to match
+ * - nextStep: the result index (>= 0) or offset (< 0) of the next node to follow. Offsets are
+ * negative and must be multiplied by -1 before being used.
+ *
+ * This structure is used to improve locality and performance when selecting from a list of
+ * options.
+ */
+ private fun buildTrieRecursive(
+ nodeOffset: Long = 0L,
+ node: Buffer,
+ byteStringOffset: Int = 0,
+ byteStrings: List<ByteString>,
+ fromIndex: Int = 0,
+ toIndex: Int = byteStrings.size,
+ indexes: List<Int>
+ ) {
+ require(fromIndex < toIndex)
+ for (i in fromIndex until toIndex) {
+ require(byteStrings[i].size >= byteStringOffset)
+ }
+
+ var fromIndex = fromIndex
+ var from = byteStrings[fromIndex]
+ val to = byteStrings[toIndex - 1]
+ var prefixIndex = -1
+
+ // If the first element is already matched, that's our prefix.
+ if (byteStringOffset == from.size) {
+ prefixIndex = indexes[fromIndex]
+ fromIndex++
+ from = byteStrings[fromIndex]
+ }
+
+ if (from[byteStringOffset] != to[byteStringOffset]) {
+ // If we have multiple bytes to choose from, encode a SELECT node.
+ var selectChoiceCount = 1
+ for (i in fromIndex + 1 until toIndex) {
+ if (byteStrings[i - 1][byteStringOffset] != byteStrings[i][byteStringOffset]) {
+ selectChoiceCount++
+ }
+ }
+
+ // Compute the offset that childNodes will get when we append it to node.
+ val childNodesOffset = nodeOffset + node.intCount + 2 + (selectChoiceCount * 2)
+
+ node.writeInt(selectChoiceCount)
+ node.writeInt(prefixIndex)
+
+ for (i in fromIndex until toIndex) {
+ val rangeByte = byteStrings[i][byteStringOffset]
+ if (i == fromIndex || rangeByte != byteStrings[i - 1][byteStringOffset]) {
+ node.writeInt(rangeByte and 0xff)
+ }
+ }
+
+ val childNodes = Buffer()
+ var rangeStart = fromIndex
+ while (rangeStart < toIndex) {
+ val rangeByte = byteStrings[rangeStart][byteStringOffset]
+ var rangeEnd = toIndex
+ for (i in rangeStart + 1 until toIndex) {
+ if (rangeByte != byteStrings[i][byteStringOffset]) {
+ rangeEnd = i
+ break
+ }
+ }
+
+ if (rangeStart + 1 == rangeEnd &&
+ byteStringOffset + 1 == byteStrings[rangeStart].size
+ ) {
+ // The result is a single index.
+ node.writeInt(indexes[rangeStart])
+ } else {
+ // The result is another node.
+ node.writeInt(-1 * (childNodesOffset + childNodes.intCount).toInt())
+ buildTrieRecursive(
+ nodeOffset = childNodesOffset,
+ node = childNodes,
+ byteStringOffset = byteStringOffset + 1,
+ byteStrings = byteStrings,
+ fromIndex = rangeStart,
+ toIndex = rangeEnd,
+ indexes = indexes
+ )
+ }
+
+ rangeStart = rangeEnd
+ }
+
+ node.writeAll(childNodes)
+ } else {
+ // If all of the bytes are the same, encode a SCAN node.
+ var scanByteCount = 0
+ for (i in byteStringOffset until minOf(from.size, to.size)) {
+ if (from[i] == to[i]) {
+ scanByteCount++
+ } else {
+ break
+ }
+ }
+
+ // Compute the offset that childNodes will get when we append it to node.
+ val childNodesOffset = nodeOffset + node.intCount + 2 + scanByteCount + 1
+
+ node.writeInt(-scanByteCount)
+ node.writeInt(prefixIndex)
+
+ for (i in byteStringOffset until byteStringOffset + scanByteCount) {
+ node.writeInt(from[i] and 0xff)
+ }
+
+ if (fromIndex + 1 == toIndex) {
+ // The result is a single index.
+ check(byteStringOffset + scanByteCount == byteStrings[fromIndex].size)
+ node.writeInt(indexes[fromIndex])
+ } else {
+ // The result is another node.
+ val childNodes = Buffer()
+ node.writeInt(-1 * (childNodesOffset + childNodes.intCount).toInt())
+ buildTrieRecursive(
+ nodeOffset = childNodesOffset,
+ node = childNodes,
+ byteStringOffset = byteStringOffset + scanByteCount,
+ byteStrings = byteStrings,
+ fromIndex = fromIndex,
+ toIndex = toIndex,
+ indexes = indexes
+ )
+ node.writeAll(childNodes)
+ }
+ }
+ }
+
+ private val Buffer.intCount get() = size / 4
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/PeekSource.kt b/okio/src/commonMain/kotlin/okio/PeekSource.kt
new file mode 100644
index 00000000..598d83de
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/PeekSource.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+/**
+ * A [Source] which peeks into an upstream [BufferedSource] and allows reading and expanding of the
+ * buffered data without consuming it. Does this by requesting additional data from the upstream
+ * source if needed and copying out of the internal buffer of the upstream source if possible.
+ *
+ * This source also maintains a snapshot of the starting location of the upstream buffer which it
+ * validates against on every read. If the upstream buffer is read from, this source will become
+ * invalid and throw [IllegalStateException] on any future reads.
+ */
+internal class PeekSource(
+ private val upstream: BufferedSource
+) : Source {
+ private val buffer = upstream.buffer
+ private var expectedSegment = buffer.head
+ private var expectedPos = buffer.head?.pos ?: -1
+
+ private var closed = false
+ private var pos = 0L
+
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ check(!closed) { "closed" }
+ // Source becomes invalid if there is an expected Segment and it and the expected position
+ // do not match the current head and head position of the upstream buffer
+ check(
+ expectedSegment == null ||
+ expectedSegment === buffer.head && expectedPos == buffer.head!!.pos
+ ) {
+ "Peek source is invalid because upstream source was used"
+ }
+ if (byteCount == 0L) return 0L
+ if (!upstream.request(pos + 1)) return -1L
+
+ if (expectedSegment == null && buffer.head != null) {
+ // Only once the buffer actually holds data should an expected Segment and position be
+ // recorded. This allows reads from the peek source to repeatedly return -1 and for data to be
+ // added later. Unit tests depend on this behavior.
+ expectedSegment = buffer.head
+ expectedPos = buffer.head!!.pos
+ }
+
+ val toCopy = minOf(byteCount, buffer.size - pos)
+ buffer.copyTo(sink, pos, toCopy)
+ pos += toCopy
+ return toCopy
+ }
+
+ override fun timeout(): Timeout {
+ return upstream.timeout()
+ }
+
+ override fun close() {
+ closed = true
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/RealBufferedSink.kt b/okio/src/commonMain/kotlin/okio/RealBufferedSink.kt
new file mode 100644
index 00000000..80e3fae4
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/RealBufferedSink.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+internal expect class RealBufferedSink(
+ sink: Sink
+) : BufferedSink {
+ val sink: Sink
+ var closed: Boolean
+}
diff --git a/okio/src/commonMain/kotlin/okio/RealBufferedSource.kt b/okio/src/commonMain/kotlin/okio/RealBufferedSource.kt
new file mode 100644
index 00000000..b626e425
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/RealBufferedSource.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+internal expect class RealBufferedSource(
+ source: Source
+) : BufferedSource {
+ val source: Source
+ var closed: Boolean
+}
diff --git a/okio/src/commonMain/kotlin/okio/Segment.kt b/okio/src/commonMain/kotlin/okio/Segment.kt
new file mode 100644
index 00000000..36879499
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/Segment.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlin.jvm.JvmField
+
+/**
+ * A segment of a buffer.
+ *
+ * Each segment in a buffer is a circularly-linked list node referencing the following and
+ * preceding segments in the buffer.
+ *
+ * Each segment in the pool is a singly-linked list node referencing the rest of segments in the
+ * pool.
+ *
+ * The underlying byte arrays of segments may be shared between buffers and byte strings. When a
+ * segment's byte array is shared the segment may not be recycled, nor may its byte data be changed.
+ * The lone exception is that the owner segment is allowed to append to the segment, writing data at
+ * `limit` and beyond. There is a single owning segment for each byte array. Positions,
+ * limits, prev, and next references are not shared.
+ */
+internal class Segment {
+ @JvmField val data: ByteArray
+
+ /** The next byte of application data byte to read in this segment. */
+ @JvmField var pos: Int = 0
+
+ /**
+ * The first byte of available data ready to be written to.
+ *
+ * If the segment is free and linked in the segment pool, the field contains total
+ * byte count of this and next segments.
+ */
+ @JvmField var limit: Int = 0
+
+ /** True if other segments or byte strings use the same byte array. */
+ @JvmField var shared: Boolean = false
+
+ /** True if this segment owns the byte array and can append to it, extending `limit`. */
+ @JvmField var owner: Boolean = false
+
+ /** Next segment in a linked or circularly-linked list. */
+ @JvmField var next: Segment? = null
+
+ /** Previous segment in a circularly-linked list. */
+ @JvmField var prev: Segment? = null
+
+ constructor() {
+ this.data = ByteArray(SIZE)
+ this.owner = true
+ this.shared = false
+ }
+
+ constructor(data: ByteArray, pos: Int, limit: Int, shared: Boolean, owner: Boolean) {
+ this.data = data
+ this.pos = pos
+ this.limit = limit
+ this.shared = shared
+ this.owner = owner
+ }
+
+ /**
+ * Returns a new segment that shares the underlying byte array with this. Adjusting pos and limit
+ * are safe but writes are forbidden. This also marks the current segment as shared, which
+ * prevents it from being pooled.
+ */
+ fun sharedCopy(): Segment {
+ shared = true
+ return Segment(data, pos, limit, true, false)
+ }
+
+ /** Returns a new segment that its own private copy of the underlying byte array. */
+ fun unsharedCopy() = Segment(data.copyOf(), pos, limit, false, true)
+
+ /**
+ * Removes this segment of a circularly-linked list and returns its successor.
+ * Returns null if the list is now empty.
+ */
+ fun pop(): Segment? {
+ val result = if (next !== this) next else null
+ prev!!.next = next
+ next!!.prev = prev
+ next = null
+ prev = null
+ return result
+ }
+
+ /**
+ * Appends `segment` after this segment in the circularly-linked list. Returns the pushed segment.
+ */
+ fun push(segment: Segment): Segment {
+ segment.prev = this
+ segment.next = next
+ next!!.prev = segment
+ next = segment
+ return segment
+ }
+
+ /**
+ * Splits this head of a circularly-linked list into two segments. The first segment contains the
+ * data in `[pos..pos+byteCount)`. The second segment contains the data in
+ * `[pos+byteCount..limit)`. This can be useful when moving partial segments from one buffer to
+ * another.
+ *
+ * Returns the new head of the circularly-linked list.
+ */
+ fun split(byteCount: Int): Segment {
+ require(byteCount > 0 && byteCount <= limit - pos) { "byteCount out of range" }
+ val prefix: Segment
+
+ // We have two competing performance goals:
+ // - Avoid copying data. We accomplish this by sharing segments.
+ // - Avoid short shared segments. These are bad for performance because they are readonly and
+ // may lead to long chains of short segments.
+ // To balance these goals we only share segments when the copy will be large.
+ if (byteCount >= SHARE_MINIMUM) {
+ prefix = sharedCopy()
+ } else {
+ prefix = SegmentPool.take()
+ data.copyInto(prefix.data, startIndex = pos, endIndex = pos + byteCount)
+ }
+
+ prefix.limit = prefix.pos + byteCount
+ pos += byteCount
+ prev!!.push(prefix)
+ return prefix
+ }
+
+ /**
+ * Call this when the tail and its predecessor may both be less than half full. This will copy
+ * data so that segments can be recycled.
+ */
+ fun compact() {
+ check(prev !== this) { "cannot compact" }
+ if (!prev!!.owner) return // Cannot compact: prev isn't writable.
+ val byteCount = limit - pos
+ val availableByteCount = SIZE - prev!!.limit + if (prev!!.shared) 0 else prev!!.pos
+ if (byteCount > availableByteCount) return // Cannot compact: not enough writable space.
+ writeTo(prev!!, byteCount)
+ pop()
+ SegmentPool.recycle(this)
+ }
+
+ /** Moves `byteCount` bytes from this segment to `sink`. */
+ fun writeTo(sink: Segment, byteCount: Int) {
+ check(sink.owner) { "only owner can write" }
+ if (sink.limit + byteCount > SIZE) {
+ // We can't fit byteCount bytes at the sink's current position. Shift sink first.
+ if (sink.shared) throw IllegalArgumentException()
+ if (sink.limit + byteCount - sink.pos > SIZE) throw IllegalArgumentException()
+ sink.data.copyInto(sink.data, startIndex = sink.pos, endIndex = sink.limit)
+ sink.limit -= sink.pos
+ sink.pos = 0
+ }
+
+ data.copyInto(
+ sink.data, destinationOffset = sink.limit, startIndex = pos,
+ endIndex = pos + byteCount
+ )
+ sink.limit += byteCount
+ pos += byteCount
+ }
+
+ companion object {
+ /** The size of all segments in bytes. */
+ const val SIZE = 8192
+
+ /** Segments will be shared when doing so avoids `arraycopy()` of this many bytes. */
+ const val SHARE_MINIMUM = 1024
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/SegmentPool.kt b/okio/src/commonMain/kotlin/okio/SegmentPool.kt
new file mode 100644
index 00000000..f21c7a29
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/SegmentPool.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * A collection of unused segments, necessary to avoid GC churn and zero-fill.
+ * This pool is a thread-safe static singleton.
+ */
+internal expect object SegmentPool {
+ val MAX_SIZE: Int
+
+ /**
+ * For testing only. Returns a snapshot of the number of bytes currently in the pool. If the pool
+ * is segmented such as by thread, this returns the byte count accessible to the calling thread.
+ */
+ val byteCount: Int
+
+ /** Return a segment for the caller's use. */
+ fun take(): Segment
+
+ /** Recycle a segment that the caller no longer needs. */
+ fun recycle(segment: Segment)
+}
diff --git a/okio/src/commonMain/kotlin/okio/SegmentedByteString.kt b/okio/src/commonMain/kotlin/okio/SegmentedByteString.kt
new file mode 100644
index 00000000..bda4f4a3
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/SegmentedByteString.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * An immutable byte string composed of segments of byte arrays. This class exists to implement
+ * efficient snapshots of buffers. It is implemented as an array of segments, plus a directory in
+ * two halves that describes how the segments compose this byte string.
+ *
+ * The first half of the directory is the cumulative byte count covered by each segment. The
+ * element at `directory[0]` contains the number of bytes held in `segments[0]`; the
+ * element at `directory[1]` contains the number of bytes held in `segments[0] +
+ * segments[1]`, and so on. The element at `directory[segments.length - 1]` contains the total
+ * size of this byte string. The first half of the directory is always monotonically increasing.
+ *
+ * The second half of the directory is the offset in `segments` of the first content byte.
+ * Bytes preceding this offset are unused, as are bytes beyond the segment's effective size.
+ *
+ * Suppose we have a byte string, `[A, B, C, D, E, F, G, H, I, J, K, L, M]` that is stored
+ * across three byte arrays: `[x, x, x, x, A, B, C, D, E, x, x, x]`, `[x, F, G]`, and `[H, I, J, K,
+ * L, M, x, x, x, x, x, x]`. The three byte arrays would be stored in `segments` in order. Since the
+ * arrays contribute 5, 2, and 6 elements respectively, the directory starts with `[5, 7, 13` to
+ * hold the cumulative total at each position. Since the offsets into the arrays are 4, 1, and 0
+ * respectively, the directory ends with `4, 1, 0]`. Concatenating these two halves, the complete
+ * directory is `[5, 7, 13, 4, 1, 0]`.
+ *
+ * This structure is chosen so that the segment holding a particular offset can be found by
+ * binary search. We use one array rather than two for the directory as a micro-optimization.
+ */
+internal expect class SegmentedByteString internal constructor(
+ segments: Array<ByteArray>,
+ directory: IntArray
+) : ByteString {
+
+ internal val segments: Array<ByteArray>
+ internal val directory: IntArray
+}
diff --git a/okio/src/commonMain/kotlin/okio/Sink.kt b/okio/src/commonMain/kotlin/okio/Sink.kt
new file mode 100644
index 00000000..0a6af54a
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/Sink.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * Receives a stream of bytes. Use this interface to write data wherever it's needed: to the
+ * network, storage, or a buffer in memory. Sinks may be layered to transform received data, such as
+ * to compress, encrypt, throttle, or add protocol framing.
+ *
+ * Most application code shouldn't operate on a sink directly, but rather on a [BufferedSink] which
+ * is both more efficient and more convenient. Use [buffer] to wrap any sink with a buffer.
+ *
+ * Sinks are easy to test: just use a [Buffer] in your tests, and read from it to confirm it
+ * received the data that was expected.
+ *
+ * ### Comparison with OutputStream
+ *
+ * This interface is functionally equivalent to [java.io.OutputStream].
+ *
+ * `OutputStream` requires multiple layers when emitted data is heterogeneous: a `DataOutputStream`
+ * for primitive values, a `BufferedOutputStream` for buffering, and `OutputStreamWriter` for
+ * charset encoding. This library uses `BufferedSink` for all of the above.
+ *
+ * Sink is also easier to layer: there is no [write()][java.io.OutputStream.write] method that is
+ * awkward to implement efficiently.
+ *
+ * ### Interop with OutputStream
+ *
+ * Use [sink] to adapt an `OutputStream` to a sink. Use [outputStream()][BufferedSink.outputStream]
+ * to adapt a sink to an `OutputStream`.
+ */
+expect interface Sink : Closeable {
+ /** Removes `byteCount` bytes from `source` and appends them to this. */
+ @Throws(IOException::class)
+ fun write(source: Buffer, byteCount: Long)
+
+ /** Pushes all buffered bytes to their final destination. */
+ @Throws(IOException::class)
+ fun flush()
+
+ /** Returns the timeout for this sink. */
+ fun timeout(): Timeout
+
+ /**
+ * Pushes all buffered bytes to their final destination and releases the resources held by this
+ * sink. It is an error to write a closed sink. It is safe to close a sink more than once.
+ */
+ @Throws(IOException::class)
+ override fun close()
+}
diff --git a/okio/src/commonMain/kotlin/okio/Source.kt b/okio/src/commonMain/kotlin/okio/Source.kt
new file mode 100644
index 00000000..97c66814
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/Source.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * Supplies a stream of bytes. Use this interface to read data from wherever it's located: from the
+ * network, storage, or a buffer in memory. Sources may be layered to transform supplied data, such
+ * as to decompress, decrypt, or remove protocol framing.
+ *
+ * Most applications shouldn't operate on a source directly, but rather on a [BufferedSource] which
+ * is both more efficient and more convenient. Use [buffer] to wrap any source with a buffer.
+ *
+ * Sources are easy to test: just use a [Buffer] in your tests, and fill it with the data your
+ * application is to read.
+ *
+ * ### Comparison with InputStream
+
+ * This interface is functionally equivalent to [java.io.InputStream].
+ *
+ * `InputStream` requires multiple layers when consumed data is heterogeneous: a `DataInputStream`
+ * for primitive values, a `BufferedInputStream` for buffering, and `InputStreamReader` for strings.
+ * This library uses `BufferedSource` for all of the above.
+ *
+ * Source avoids the impossible-to-implement [available()][java.io.InputStream.available] method.
+ * Instead callers specify how many bytes they [require][BufferedSource.require].
+ *
+ * Source omits the unsafe-to-compose [mark and reset][java.io.InputStream.mark] state that's
+ * tracked by `InputStream`; instead, callers just buffer what they need.
+ *
+ * When implementing a source, you don't need to worry about the [read()][java.io.InputStream.read]
+ * method that is awkward to implement efficiently and returns one of 257 possible values.
+ *
+ * And source has a stronger `skip` method: [BufferedSource.skip] won't return prematurely.
+ *
+ * ### Interop with InputStream
+ *
+ * Use [source] to adapt an `InputStream` to a source. Use [BufferedSource.inputStream] to adapt a
+ * source to an `InputStream`.
+ */
+interface Source : Closeable {
+ /**
+ * Removes at least 1, and up to `byteCount` bytes from this and appends them to `sink`. Returns
+ * the number of bytes read, or -1 if this source is exhausted.
+ */
+ @Throws(IOException::class)
+ fun read(sink: Buffer, byteCount: Long): Long
+
+ /** Returns the timeout for this source. */
+ fun timeout(): Timeout
+
+ /**
+ * Closes this source and releases the resources held by this source. It is an error to read a
+ * closed source. It is safe to close a source more than once.
+ */
+ @Throws(IOException::class)
+ override fun close()
+}
diff --git a/okio/src/commonMain/kotlin/okio/Timeout.kt b/okio/src/commonMain/kotlin/okio/Timeout.kt
new file mode 100644
index 00000000..62600bbc
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/Timeout.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * A policy on how much time to spend on a task before giving up. When a task times out, it is left
+ * in an unspecified state and should be abandoned. For example, if reading from a source times out,
+ * that source should be closed and the read should be retried later. If writing to a sink times
+ * out, the same rules apply: close the sink and retry later.
+ *
+ * ### Timeouts and Deadlines
+ *
+ * This class offers two complementary controls to define a timeout policy.
+ *
+ * **Timeouts** specify the maximum time to wait for a single operation to complete. Timeouts are
+ * typically used to detect problems like network partitions. For example, if a remote peer doesn't
+ * return *any* data for ten seconds, we may assume that the peer is unavailable.
+ *
+ * **Deadlines** specify the maximum time to spend on a job, composed of one or more operations. Use
+ * deadlines to set an upper bound on the time invested on a job. For example, a battery-conscious
+ * app may limit how much time it spends pre-loading content.
+ */
+expect open class Timeout {
+ companion object {
+ /**
+ * An empty timeout that neither tracks nor detects timeouts. Use this when timeouts aren't
+ * necessary, such as in implementations whose operations do not block.
+ */
+ val NONE: Timeout
+ }
+}
diff --git a/okio/src/commonMain/kotlin/okio/Utf8.kt b/okio/src/commonMain/kotlin/okio/Utf8.kt
new file mode 100644
index 00000000..ca20e214
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/Utf8.kt
@@ -0,0 +1,557 @@
+/*
+ * Copyright (C) 2017 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Okio assumes most applications use UTF-8 exclusively, and offers optimized implementations of
+ * common operations on UTF-8 strings.
+ *
+ * <table border="1" cellspacing="0" cellpadding="3" summary="">
+ * <tr>
+ * <th></th>
+ * <th>[ByteString]</th>
+ * <th>[Buffer], [BufferedSink], [BufferedSource]</th>
+ * </tr>
+ * <tr>
+ * <td>Encode a string</td>
+ * <td>[ByteString.encodeUtf8]</td>
+ * <td>[BufferedSink.writeUtf8]</td>
+ * </tr>
+ * <tr>
+ * <td>Encode a code point</td>
+ * <td></td>
+ * <td>[BufferedSink.writeUtf8CodePoint]</td>
+ * </tr>
+ * <tr>
+ * <td>Decode a string</td>
+ * <td>[ByteString.utf8]</td>
+ * <td>[BufferedSource.readUtf8], [BufferedSource.readUtf8]</td>
+ * </tr>
+ * <tr>
+ * <td>Decode a code point</td>
+ * <td></td>
+ * <td>[BufferedSource.readUtf8CodePoint]</td>
+ * </tr>
+ * <tr>
+ * <td>Decode until the next `\r\n` or `\n`</td>
+ * <td></td>
+ * <td>[BufferedSource.readUtf8LineStrict],
+ * [BufferedSource.readUtf8LineStrict]</td>
+ * </tr>
+ * <tr>
+ * <td>Decode until the next `\r\n`, `\n`, or `EOF`</td>
+ * <td></td>
+ * <td>[BufferedSource.readUtf8Line]</td>
+ * </tr>
+ * <tr>
+ * <td>Measure the bytes in a UTF-8 string</td>
+ * <td colspan="2">[Utf8.size], [Utf8.size]</td>
+ * </tr>
+ * </table>
+ */
+@file:JvmName("Utf8")
+
+package okio
+
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmOverloads
+
+/**
+ * Returns the number of bytes used to encode the slice of `string` as UTF-8 when using
+ * [BufferedSink.writeUtf8].
+ */
+@JvmOverloads
+@JvmName("size")
+fun String.utf8Size(beginIndex: Int = 0, endIndex: Int = length): Long {
+ require(beginIndex >= 0) { "beginIndex < 0: $beginIndex" }
+ require(endIndex >= beginIndex) { "endIndex < beginIndex: $endIndex < $beginIndex" }
+ require(endIndex <= length) { "endIndex > string.length: $endIndex > $length" }
+
+ var result = 0L
+ var i = beginIndex
+ while (i < endIndex) {
+ val c = this[i].toInt()
+
+ if (c < 0x80) {
+ // A 7-bit character with 1 byte.
+ result++
+ i++
+ } else if (c < 0x800) {
+ // An 11-bit character with 2 bytes.
+ result += 2
+ i++
+ } else if (c < 0xd800 || c > 0xdfff) {
+ // A 16-bit character with 3 bytes.
+ result += 3
+ i++
+ } else {
+ val low = if (i + 1 < endIndex) this[i + 1].toInt() else 0
+ if (c > 0xdbff || low < 0xdc00 || low > 0xdfff) {
+ // A malformed surrogate, which yields '?'.
+ result++
+ i++
+ } else {
+ // A 21-bit character with 4 bytes.
+ result += 4
+ i += 2
+ }
+ }
+ }
+
+ return result
+}
+
+internal const val REPLACEMENT_BYTE: Byte = '?'.toByte()
+internal const val REPLACEMENT_CHARACTER: Char = '\ufffd'
+internal const val REPLACEMENT_CODE_POINT: Int = REPLACEMENT_CHARACTER.toInt()
+
+@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
+internal inline fun isIsoControl(codePoint: Int): Boolean =
+ (codePoint in 0x00..0x1F) || (codePoint in 0x7F..0x9F)
+
+@Suppress("NOTHING_TO_INLINE") // Syntactic sugar.
+internal inline fun isUtf8Continuation(byte: Byte): Boolean {
+ // 0b10xxxxxx
+ return byte and 0xc0 == 0x80
+}
+
+// TODO combine with Buffer.writeUtf8?
+// TODO combine with Buffer.writeUtf8CodePoint?
+internal inline fun String.processUtf8Bytes(
+ beginIndex: Int,
+ endIndex: Int,
+ yield: (Byte) -> Unit
+) {
+ // Transcode a UTF-16 String to UTF-8 bytes.
+ var index = beginIndex
+ while (index < endIndex) {
+ val c = this[index]
+
+ when {
+ c < '\u0080' -> {
+ // Emit a 7-bit character with 1 byte.
+ yield(c.toByte()) // 0xxxxxxx
+ index++
+
+ // Assume there is going to be more ASCII
+ while (index < endIndex && this[index] < '\u0080') {
+ yield(this[index++].toByte())
+ }
+ }
+
+ c < '\u0800' -> {
+ // Emit a 11-bit character with 2 bytes.
+ /* ktlint-disable no-multi-spaces */
+ yield((c.toInt() shr 6 or 0xc0).toByte()) // 110xxxxx
+ yield((c.toInt() and 0x3f or 0x80).toByte()) // 10xxxxxx
+ /* ktlint-enable no-multi-spaces */
+ index++
+ }
+
+ c !in '\ud800'..'\udfff' -> {
+ // Emit a 16-bit character with 3 bytes.
+ /* ktlint-disable no-multi-spaces */
+ yield((c.toInt() shr 12 or 0xe0).toByte()) // 1110xxxx
+ yield((c.toInt() shr 6 and 0x3f or 0x80).toByte()) // 10xxxxxx
+ yield((c.toInt() and 0x3f or 0x80).toByte()) // 10xxxxxx
+ /* ktlint-enable no-multi-spaces */
+ index++
+ }
+
+ else -> {
+ // c is a surrogate. Make sure it is a high surrogate & that its successor is a low
+ // surrogate. If not, the UTF-16 is invalid, in which case we emit a replacement
+ // byte.
+ if (c > '\udbff' ||
+ endIndex <= index + 1 ||
+ this[index + 1] !in '\udc00'..'\udfff'
+ ) {
+ yield(REPLACEMENT_BYTE)
+ index++
+ } else {
+ // UTF-16 high surrogate: 110110xxxxxxxxxx (10 bits)
+ // UTF-16 low surrogate: 110111yyyyyyyyyy (10 bits)
+ // Unicode code point: 00010000000000000000 + xxxxxxxxxxyyyyyyyyyy (21 bits)
+ val codePoint = (
+ ((c.toInt() shl 10) + this[index + 1].toInt()) +
+ (0x010000 - (0xd800 shl 10) - 0xdc00)
+ )
+
+ // Emit a 21-bit character with 4 bytes.
+ /* ktlint-disable no-multi-spaces */
+ yield((codePoint shr 18 or 0xf0).toByte()) // 11110xxx
+ yield((codePoint shr 12 and 0x3f or 0x80).toByte()) // 10xxxxxx
+ yield((codePoint shr 6 and 0x3f or 0x80).toByte()) // 10xxyyyy
+ yield((codePoint and 0x3f or 0x80).toByte()) // 10yyyyyy
+ /* ktlint-enable no-multi-spaces */
+ index += 2
+ }
+ }
+ }
+ }
+}
+
+// TODO combine with Buffer.readUtf8CodePoint?
+internal inline fun ByteArray.processUtf8CodePoints(
+ beginIndex: Int,
+ endIndex: Int,
+ yield: (Int) -> Unit
+) {
+ var index = beginIndex
+ while (index < endIndex) {
+ val b0 = this[index]
+ when {
+ b0 >= 0 -> {
+ // 0b0xxxxxxx
+ yield(b0.toInt())
+ index++
+
+ // Assume there is going to be more ASCII
+ while (index < endIndex && this[index] >= 0) {
+ yield(this[index++].toInt())
+ }
+ }
+ b0 shr 5 == -2 -> {
+ // 0b110xxxxx
+ index += process2Utf8Bytes(index, endIndex) { yield(it) }
+ }
+ b0 shr 4 == -2 -> {
+ // 0b1110xxxx
+ index += process3Utf8Bytes(index, endIndex) { yield(it) }
+ }
+ b0 shr 3 == -2 -> {
+ // 0b11110xxx
+ index += process4Utf8Bytes(index, endIndex) { yield(it) }
+ }
+ else -> {
+ // 0b10xxxxxx - Unexpected continuation
+ // 0b111111xxx - Unknown encoding
+ yield(REPLACEMENT_CODE_POINT)
+ index++
+ }
+ }
+ }
+}
+
+// Value added to the high UTF-16 surrogate after shifting
+internal const val HIGH_SURROGATE_HEADER = 0xd800 - (0x010000 ushr 10)
+// Value added to the low UTF-16 surrogate after masking
+internal const val LOG_SURROGATE_HEADER = 0xdc00
+
+// TODO combine with Buffer.readUtf8?
+internal inline fun ByteArray.processUtf16Chars(
+ beginIndex: Int,
+ endIndex: Int,
+ yield: (Char) -> Unit
+) {
+ var index = beginIndex
+ while (index < endIndex) {
+ val b0 = this[index]
+ when {
+ b0 >= 0 -> {
+ // 0b0xxxxxxx
+ yield(b0.toChar())
+ index++
+
+ // Assume there is going to be more ASCII
+ // This is almost double the performance of the outer loop
+ while (index < endIndex && this[index] >= 0) {
+ yield(this[index++].toChar())
+ }
+ }
+ b0 shr 5 == -2 -> {
+ // 0b110xxxxx
+ index += process2Utf8Bytes(index, endIndex) { yield(it.toChar()) }
+ }
+ b0 shr 4 == -2 -> {
+ // 0b1110xxxx
+ index += process3Utf8Bytes(index, endIndex) { yield(it.toChar()) }
+ }
+ b0 shr 3 == -2 -> {
+ // 0b11110xxx
+ index += process4Utf8Bytes(index, endIndex) { codePoint ->
+ if (codePoint != REPLACEMENT_CODE_POINT) {
+ // Unicode code point: 00010000000000000000 + xxxxxxxxxxyyyyyyyyyy (21 bits)
+ // UTF-16 high surrogate: 110110xxxxxxxxxx (10 bits)
+ // UTF-16 low surrogate: 110111yyyyyyyyyy (10 bits)
+ /* ktlint-disable no-multi-spaces paren-spacing */
+ yield(((codePoint ushr 10 ) + HIGH_SURROGATE_HEADER).toChar())
+ /* ktlint-enable no-multi-spaces paren-spacing */
+ yield(((codePoint and 0x03ff) + LOG_SURROGATE_HEADER).toChar())
+ } else {
+ yield(REPLACEMENT_CHARACTER)
+ }
+ }
+ }
+ else -> {
+ // 0b10xxxxxx - Unexpected continuation
+ // 0b111111xxx - Unknown encoding
+ yield(REPLACEMENT_CHARACTER)
+ index++
+ }
+ }
+ }
+}
+
+// ===== UTF-8 Encoding and Decoding ===== //
+/*
+The following 3 methods take advantage of using XOR on 2's complement store
+numbers to quickly and efficiently combine the important data of UTF-8 encoded
+bytes. This will be best explained using an example, so lets take the following
+encoded character '∇' = \u2207.
+
+Using the Unicode code point for this character, 0x2207, we will split the
+binary representation into 3 sections as follows:
+
+ 0x2207 = 0b0010 0010 0000 0111
+ xxxx yyyy yyzz zzzz
+
+Now take each section of bits and add the appropriate header:
+
+ utf8(0x2207) = 0b1110 xxxx 0b10yy yyyy 0b10zz zzzz
+ = 0b1110 0010 0b1000 1000 0b1000 0111
+ = 0xe2 0x88 0x87
+
+We have now just encoded this as a 3 byte UTF-8 character. More information
+about different sizes of characters can be found here:
+ https://en.wikipedia.org/wiki/UTF-8
+
+Encoding was pretty easy, but decoding is a bit more complicated. We need to
+first determine the number of bytes used to represent the character, strip all
+the headers, and then combine all the bits into a single integer. Let's use the
+character we just encoded and work backwards, taking advantage of 2's complement
+integer representation and the XOR function.
+
+Let's look at the decimal representation of these bytes:
+
+ 0xe2, 0x88, 0x87 = -30, -120, -121
+
+The first interesting thing to notice is that UTF-8 headers all start with 1 -
+except for ASCII which is encoded as a single byte - which means all UTF-8 bytes
+will be negative. So converting these to integers results in a lot of 1's added
+because they are store as 2's complement:
+
+ 0xe2 = -30 = 0xffff ffe2
+ 0x88 = -120 = 0xffff ff88
+ 0x87 = -121 = 0xffff ff87
+
+Now let's XOR these with their corresponding UTF-8 byte headers to see what
+happens:
+
+ 0xffff ffe2 xor 0xffff ffe0 = 0x0000 0002
+ 0xffff ff88 xor 0xffff ff80 = 0x0000 0008
+ 0xffff ff87 xor 0xffff ff80 = 0x0000 0007
+
+***This is why we must first convert the byte header mask to a byte and then
+back to an integer, so it is properly converted to a 2's complement negative
+number which can be applied to each byte.***
+
+Now let's look at the binary representation to see how we can combine these to
+create the Unicode code point:
+
+ 0b0000 0010 0b0000 1000 0b0000 0111
+ 0b1110 xxxx 0b10yy yyyy 0b10zz zzzz
+
+Combining each section will require some bit shifting, but then they can just
+be OR'd together. They can also be XOR'd together which makes use of a single,
+COMMUTATIVE, operator through the entire calculation.
+
+ << 12 = 00000010
+ << 6 = 00001000
+ << 0 = 00000111
+ XOR = 00000010001000000111
+
+ code point = 0b0010 0010 0000 0111
+ = 0x2207
+
+And there we have it! The decoded UTF-8 character '∇'! And because the XOR
+operator is commutative, we can re-arrange all this XOR and shifting to create
+a single mask that can be applied to 3-byte UTF-8 characters after their bytes
+have been shifted and XOR'd together.
+ */
+
+// Mask used to remove byte headers from a 2 byte encoded UTF-8 character
+internal const val MASK_2BYTES = 0x0f80
+// MASK_2BYTES =
+// (0xc0.toByte() shl 6) xor
+// (0x80.toByte().toInt())
+
+internal inline fun ByteArray.process2Utf8Bytes(
+ beginIndex: Int,
+ endIndex: Int,
+ yield: (Int) -> Unit
+): Int {
+ if (endIndex <= beginIndex + 1) {
+ yield(REPLACEMENT_CODE_POINT)
+ // Only 1 byte remaining - underflow
+ return 1
+ }
+
+ val b0 = this[beginIndex]
+ val b1 = this[beginIndex + 1]
+ if (!isUtf8Continuation(b1)) {
+ yield(REPLACEMENT_CODE_POINT)
+ return 1
+ }
+
+ val codePoint =
+ (
+ MASK_2BYTES
+ xor (b1.toInt())
+ xor (b0.toInt() shl 6)
+ )
+
+ when {
+ codePoint < 0x80 -> {
+ yield(REPLACEMENT_CODE_POINT) // Reject overlong code points.
+ }
+ else -> {
+ yield(codePoint)
+ }
+ }
+ return 2
+}
+
+// Mask used to remove byte headers from a 3 byte encoded UTF-8 character
+internal const val MASK_3BYTES = -0x01e080
+// MASK_3BYTES =
+// (0xe0.toByte() shl 12) xor
+// (0x80.toByte() shl 6) xor
+// (0x80.toByte().toInt())
+
+internal inline fun ByteArray.process3Utf8Bytes(
+ beginIndex: Int,
+ endIndex: Int,
+ yield: (Int) -> Unit
+): Int {
+ if (endIndex <= beginIndex + 2) {
+ // At least 2 bytes remaining
+ yield(REPLACEMENT_CODE_POINT)
+ if (endIndex <= beginIndex + 1 || !isUtf8Continuation(this[beginIndex + 1])) {
+ // Only 1 byte remaining - underflow
+ // Or 2nd byte is not a continuation - malformed
+ return 1
+ } else {
+ // Only 2 bytes remaining - underflow
+ return 2
+ }
+ }
+
+ val b0 = this[beginIndex]
+ val b1 = this[beginIndex + 1]
+ if (!isUtf8Continuation(b1)) {
+ yield(REPLACEMENT_CODE_POINT)
+ return 1
+ }
+ val b2 = this[beginIndex + 2]
+ if (!isUtf8Continuation(b2)) {
+ yield(REPLACEMENT_CODE_POINT)
+ return 2
+ }
+
+ val codePoint =
+ (
+ MASK_3BYTES
+ xor (b2.toInt())
+ xor (b1.toInt() shl 6)
+ xor (b0.toInt() shl 12)
+ )
+
+ when {
+ codePoint < 0x800 -> {
+ yield(REPLACEMENT_CODE_POINT) // Reject overlong code points.
+ }
+ codePoint in 0xd800..0xdfff -> {
+ yield(REPLACEMENT_CODE_POINT) // Reject partial surrogates.
+ }
+ else -> {
+ yield(codePoint)
+ }
+ }
+ return 3
+}
+
+// Mask used to remove byte headers from a 4 byte encoded UTF-8 character
+internal const val MASK_4BYTES = 0x381f80
+// MASK_4BYTES =
+// (0xf0.toByte() shl 18) xor
+// (0x80.toByte() shl 12) xor
+// (0x80.toByte() shl 6) xor
+// (0x80.toByte().toInt())
+
+internal inline fun ByteArray.process4Utf8Bytes(
+ beginIndex: Int,
+ endIndex: Int,
+ yield: (Int) -> Unit
+): Int {
+ if (endIndex <= beginIndex + 3) {
+ // At least 3 bytes remaining
+ yield(REPLACEMENT_CODE_POINT)
+ if (endIndex <= beginIndex + 1 || !isUtf8Continuation(this[beginIndex + 1])) {
+ // Only 1 byte remaining - underflow
+ // Or 2nd byte is not a continuation - malformed
+ return 1
+ } else if (endIndex <= beginIndex + 2 || !isUtf8Continuation(this[beginIndex + 2])) {
+ // Only 2 bytes remaining - underflow
+ // Or 3rd byte is not a continuation - malformed
+ return 2
+ } else {
+ // Only 3 bytes remaining - underflow
+ return 3
+ }
+ }
+
+ val b0 = this[beginIndex]
+ val b1 = this[beginIndex + 1]
+ if (!isUtf8Continuation(b1)) {
+ yield(REPLACEMENT_CODE_POINT)
+ return 1
+ }
+ val b2 = this[beginIndex + 2]
+ if (!isUtf8Continuation(b2)) {
+ yield(REPLACEMENT_CODE_POINT)
+ return 2
+ }
+ val b3 = this[beginIndex + 3]
+ if (!isUtf8Continuation(b3)) {
+ yield(REPLACEMENT_CODE_POINT)
+ return 3
+ }
+
+ val codePoint =
+ (
+ MASK_4BYTES
+ xor (b3.toInt())
+ xor (b2.toInt() shl 6)
+ xor (b1.toInt() shl 12)
+ xor (b0.toInt() shl 18)
+ )
+
+ when {
+ codePoint > 0x10ffff -> {
+ yield(REPLACEMENT_CODE_POINT) // Reject code points larger than the Unicode maximum.
+ }
+ codePoint in 0xd800..0xdfff -> {
+ yield(REPLACEMENT_CODE_POINT) // Reject partial surrogates.
+ }
+ codePoint < 0x10000 -> {
+ yield(REPLACEMENT_CODE_POINT) // Reject overlong code points.
+ }
+ else -> {
+ yield(codePoint)
+ }
+ }
+ return 4
+}
diff --git a/okio/src/commonMain/kotlin/okio/internal/-Utf8.kt b/okio/src/commonMain/kotlin/okio/internal/-Utf8.kt
new file mode 100644
index 00000000..926f1853
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/internal/-Utf8.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio.internal
+
+import okio.ArrayIndexOutOfBoundsException
+import okio.processUtf16Chars
+import okio.processUtf8Bytes
+
+// TODO For benchmarking, these methods need to be available but preferably invisible
+// to everything else. Putting them in this file, `-Utf8.kt`, makes them invisible to
+// Java but still visible to Kotlin.
+
+fun ByteArray.commonToUtf8String(beginIndex: Int = 0, endIndex: Int = size): String {
+ if (beginIndex < 0 || endIndex > size || beginIndex > endIndex) {
+ throw ArrayIndexOutOfBoundsException("size=$size beginIndex=$beginIndex endIndex=$endIndex")
+ }
+ val chars = CharArray(endIndex - beginIndex)
+
+ var length = 0
+ processUtf16Chars(beginIndex, endIndex) { c ->
+ chars[length++] = c
+ }
+
+ return String(chars, 0, length)
+}
+
+fun String.commonAsUtf8ToByteArray(): ByteArray {
+ val bytes = ByteArray(4 * length)
+
+ // Assume ASCII until a UTF-8 code point is observed. This is ugly but yields
+ // about a 2x performance increase for pure ASCII.
+ for (index in 0 until length) {
+ val b0 = this[index]
+ if (b0 >= '\u0080') {
+ var size = index
+ processUtf8Bytes(index, length) { c ->
+ bytes[size++] = c
+ }
+ return bytes.copyOf(size)
+ }
+ bytes[index] = b0.toByte()
+ }
+
+ return bytes.copyOf(length)
+}
diff --git a/okio/src/commonMain/kotlin/okio/internal/Buffer.kt b/okio/src/commonMain/kotlin/okio/internal/Buffer.kt
new file mode 100644
index 00000000..0cb15cc4
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/internal/Buffer.kt
@@ -0,0 +1,1688 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO move to Buffer class: https://youtrack.jetbrains.com/issue/KT-20427
+@file:Suppress("NOTHING_TO_INLINE")
+
+package okio.internal
+
+import okio.ArrayIndexOutOfBoundsException
+import okio.Buffer
+import okio.Buffer.UnsafeCursor
+import okio.ByteString
+import okio.EOFException
+import okio.Options
+import okio.REPLACEMENT_CODE_POINT
+import okio.Segment
+import okio.SegmentPool
+import okio.SegmentedByteString
+import okio.Sink
+import okio.Source
+import okio.and
+import okio.asUtf8ToByteArray
+import okio.checkOffsetAndCount
+import okio.minOf
+import okio.toHexString
+
+internal val HEX_DIGIT_BYTES = "0123456789abcdef".asUtf8ToByteArray()
+
+// Threshold determined empirically via ReadByteStringBenchmark
+/** Create SegmentedByteString when size is greater than this many bytes. */
+internal const val SEGMENTING_THRESHOLD = 4096
+
+/**
+ * Returns true if the range within this buffer starting at `segmentPos` in `segment` is equal to
+ * `bytes[bytesOffset..bytesLimit)`.
+ */
+internal fun rangeEquals(
+ segment: Segment,
+ segmentPos: Int,
+ bytes: ByteArray,
+ bytesOffset: Int,
+ bytesLimit: Int
+): Boolean {
+ var segment = segment
+ var segmentPos = segmentPos
+ var segmentLimit = segment.limit
+ var data = segment.data
+
+ var i = bytesOffset
+ while (i < bytesLimit) {
+ if (segmentPos == segmentLimit) {
+ segment = segment.next!!
+ data = segment.data
+ segmentPos = segment.pos
+ segmentLimit = segment.limit
+ }
+
+ if (data[segmentPos] != bytes[i]) {
+ return false
+ }
+
+ segmentPos++
+ i++
+ }
+
+ return true
+}
+
+internal fun Buffer.readUtf8Line(newline: Long): String {
+ return when {
+ newline > 0 && this[newline - 1] == '\r'.toByte() -> {
+ // Read everything until '\r\n', then skip the '\r\n'.
+ val result = readUtf8(newline - 1L)
+ skip(2L)
+ result
+ }
+ else -> {
+ // Read everything until '\n', then skip the '\n'.
+ val result = readUtf8(newline)
+ skip(1L)
+ result
+ }
+ }
+}
+
+/**
+ * Invoke `lambda` with the segment and offset at `fromIndex`. Searches from the front or the back
+ * depending on what's closer to `fromIndex`.
+ */
+internal inline fun <T> Buffer.seek(
+ fromIndex: Long,
+ lambda: (Segment?, Long) -> T
+): T {
+ var s: Segment = head ?: return lambda(null, -1L)
+
+ if (size - fromIndex < fromIndex) {
+ // We're scanning in the back half of this buffer. Find the segment starting at the back.
+ var offset = size
+ while (offset > fromIndex) {
+ s = s.prev!!
+ offset -= (s.limit - s.pos).toLong()
+ }
+ return lambda(s, offset)
+ } else {
+ // We're scanning in the front half of this buffer. Find the segment starting at the front.
+ var offset = 0L
+ while (true) {
+ val nextOffset = offset + (s.limit - s.pos)
+ if (nextOffset > fromIndex) break
+ s = s.next!!
+ offset = nextOffset
+ }
+ return lambda(s, offset)
+ }
+}
+
+/**
+ * Returns the index of a value in options that is a prefix of this buffer. Returns -1 if no value
+ * is found. This method does two simultaneous iterations: it iterates the trie and it iterates
+ * this buffer. It returns when it reaches a result in the trie, when it mismatches in the trie,
+ * and when the buffer is exhausted.
+ *
+ * @param selectTruncated true to return -2 if a possible result is present but truncated. For
+ * example, this will return -2 if the buffer contains [ab] and the options are [abc, abd].
+ * Note that this is made complicated by the fact that options are listed in preference order,
+ * and one option may be a prefix of another. For example, this returns -2 if the buffer
+ * contains [ab] and the options are [abc, a].
+ */
+internal fun Buffer.selectPrefix(options: Options, selectTruncated: Boolean = false): Int {
+ val head = head ?: return if (selectTruncated) -2 else -1
+
+ var s: Segment? = head
+ var data = head.data
+ var pos = head.pos
+ var limit = head.limit
+
+ val trie = options.trie
+ var triePos = 0
+
+ var prefixIndex = -1
+
+ navigateTrie@
+ while (true) {
+ val scanOrSelect = trie[triePos++]
+
+ val possiblePrefixIndex = trie[triePos++]
+ if (possiblePrefixIndex != -1) {
+ prefixIndex = possiblePrefixIndex
+ }
+
+ val nextStep: Int
+
+ if (s == null) {
+ break@navigateTrie
+ } else if (scanOrSelect < 0) {
+ // Scan: take multiple bytes from the buffer and the trie, looking for any mismatch.
+ val scanByteCount = -1 * scanOrSelect
+ val trieLimit = triePos + scanByteCount
+ while (true) {
+ val byte = data[pos++] and 0xff
+ if (byte != trie[triePos++]) return prefixIndex // Fail 'cause we found a mismatch.
+ val scanComplete = (triePos == trieLimit)
+
+ // Advance to the next buffer segment if this one is exhausted.
+ if (pos == limit) {
+ s = s!!.next!!
+ pos = s.pos
+ data = s.data
+ limit = s.limit
+ if (s === head) {
+ if (!scanComplete) break@navigateTrie // We were exhausted before the scan completed.
+ s = null // We were exhausted at the end of the scan.
+ }
+ }
+
+ if (scanComplete) {
+ nextStep = trie[triePos]
+ break
+ }
+ }
+ } else {
+ // Select: take one byte from the buffer and find a match in the trie.
+ val selectChoiceCount = scanOrSelect
+ val byte = data[pos++] and 0xff
+ val selectLimit = triePos + selectChoiceCount
+ while (true) {
+ if (triePos == selectLimit) return prefixIndex // Fail 'cause we didn't find a match.
+
+ if (byte == trie[triePos]) {
+ nextStep = trie[triePos + selectChoiceCount]
+ break
+ }
+
+ triePos++
+ }
+
+ // Advance to the next buffer segment if this one is exhausted.
+ if (pos == limit) {
+ s = s.next!!
+ pos = s.pos
+ data = s.data
+ limit = s.limit
+ if (s === head) {
+ s = null // No more segments! The next trie node will be our last.
+ }
+ }
+ }
+
+ if (nextStep >= 0) return nextStep // Found a matching option.
+ triePos = -nextStep // Found another node to continue the search.
+ }
+
+ // We break out of the loop above when we've exhausted the buffer without exhausting the trie.
+ if (selectTruncated) return -2 // The buffer is a prefix of at least one option.
+ return prefixIndex // Return any matches we encountered while searching for a deeper match.
+}
+
+// TODO Kotlin's expect classes can't have default implementations, so platform implementations
+// have to call these functions. Remove all this nonsense when expect class allow actual code.
+
+internal inline fun Buffer.commonCopyTo(
+ out: Buffer,
+ offset: Long,
+ byteCount: Long
+): Buffer {
+ var offset = offset
+ var byteCount = byteCount
+ checkOffsetAndCount(size, offset, byteCount)
+ if (byteCount == 0L) return this
+
+ out.size += byteCount
+
+ // Skip segments that we aren't copying from.
+ var s = head
+ while (offset >= s!!.limit - s.pos) {
+ offset -= (s.limit - s.pos).toLong()
+ s = s.next
+ }
+
+ // Copy one segment at a time.
+ while (byteCount > 0L) {
+ val copy = s!!.sharedCopy()
+ copy.pos += offset.toInt()
+ copy.limit = minOf(copy.pos + byteCount.toInt(), copy.limit)
+ if (out.head == null) {
+ copy.prev = copy
+ copy.next = copy.prev
+ out.head = copy.next
+ } else {
+ out.head!!.prev!!.push(copy)
+ }
+ byteCount -= (copy.limit - copy.pos).toLong()
+ offset = 0L
+ s = s.next
+ }
+
+ return this
+}
+
+internal inline fun Buffer.commonCompleteSegmentByteCount(): Long {
+ var result = size
+ if (result == 0L) return 0L
+
+ // Omit the tail if it's still writable.
+ val tail = head!!.prev!!
+ if (tail.limit < Segment.SIZE && tail.owner) {
+ result -= (tail.limit - tail.pos).toLong()
+ }
+
+ return result
+}
+
+internal inline fun Buffer.commonReadByte(): Byte {
+ if (size == 0L) throw EOFException()
+
+ val segment = head!!
+ var pos = segment.pos
+ val limit = segment.limit
+
+ val data = segment.data
+ val b = data[pos++]
+ size -= 1L
+
+ if (pos == limit) {
+ head = segment.pop()
+ SegmentPool.recycle(segment)
+ } else {
+ segment.pos = pos
+ }
+
+ return b
+}
+
+internal inline fun Buffer.commonReadShort(): Short {
+ if (size < 2L) throw EOFException()
+
+ val segment = head!!
+ var pos = segment.pos
+ val limit = segment.limit
+
+ // If the short is split across multiple segments, delegate to readByte().
+ if (limit - pos < 2) {
+ val s = readByte() and 0xff shl 8 or (readByte() and 0xff)
+ return s.toShort()
+ }
+
+ val data = segment.data
+ val s = data[pos++] and 0xff shl 8 or (data[pos++] and 0xff)
+ size -= 2L
+
+ if (pos == limit) {
+ head = segment.pop()
+ SegmentPool.recycle(segment)
+ } else {
+ segment.pos = pos
+ }
+
+ return s.toShort()
+}
+
+internal inline fun Buffer.commonReadInt(): Int {
+ if (size < 4L) throw EOFException()
+
+ val segment = head!!
+ var pos = segment.pos
+ val limit = segment.limit
+
+ // If the int is split across multiple segments, delegate to readByte().
+ if (limit - pos < 4L) {
+ return (
+ readByte() and 0xff shl 24
+ or (readByte() and 0xff shl 16)
+ or (readByte() and 0xff shl 8) // ktlint-disable no-multi-spaces
+ or (readByte() and 0xff)
+ )
+ }
+
+ val data = segment.data
+ val i = (
+ data[pos++] and 0xff shl 24
+ or (data[pos++] and 0xff shl 16)
+ or (data[pos++] and 0xff shl 8)
+ or (data[pos++] and 0xff)
+ )
+ size -= 4L
+
+ if (pos == limit) {
+ head = segment.pop()
+ SegmentPool.recycle(segment)
+ } else {
+ segment.pos = pos
+ }
+
+ return i
+}
+
+internal inline fun Buffer.commonReadLong(): Long {
+ if (size < 8L) throw EOFException()
+
+ val segment = head!!
+ var pos = segment.pos
+ val limit = segment.limit
+
+ // If the long is split across multiple segments, delegate to readInt().
+ if (limit - pos < 8L) {
+ return (
+ readInt() and 0xffffffffL shl 32
+ or (readInt() and 0xffffffffL)
+ )
+ }
+
+ val data = segment.data
+ val v = (
+ data[pos++] and 0xffL shl 56
+ or (data[pos++] and 0xffL shl 48)
+ or (data[pos++] and 0xffL shl 40)
+ or (data[pos++] and 0xffL shl 32)
+ or (data[pos++] and 0xffL shl 24)
+ or (data[pos++] and 0xffL shl 16)
+ or (data[pos++] and 0xffL shl 8) // ktlint-disable no-multi-spaces
+ or (data[pos++] and 0xffL)
+ )
+ size -= 8L
+
+ if (pos == limit) {
+ head = segment.pop()
+ SegmentPool.recycle(segment)
+ } else {
+ segment.pos = pos
+ }
+
+ return v
+}
+
+internal inline fun Buffer.commonGet(pos: Long): Byte {
+ checkOffsetAndCount(size, pos, 1L)
+ seek(pos) { s, offset ->
+ return s!!.data[(s.pos + pos - offset).toInt()]
+ }
+}
+
+internal inline fun Buffer.commonClear() = skip(size)
+
+internal inline fun Buffer.commonSkip(byteCount: Long) {
+ var byteCount = byteCount
+ while (byteCount > 0) {
+ val head = this.head ?: throw EOFException()
+
+ val toSkip = minOf(byteCount, head.limit - head.pos).toInt()
+ size -= toSkip.toLong()
+ byteCount -= toSkip.toLong()
+ head.pos += toSkip
+
+ if (head.pos == head.limit) {
+ this.head = head.pop()
+ SegmentPool.recycle(head)
+ }
+ }
+}
+
+internal inline fun Buffer.commonWrite(
+ byteString: ByteString,
+ offset: Int = 0,
+ byteCount: Int = byteString.size
+): Buffer {
+ byteString.write(this, offset, byteCount)
+ return this
+}
+
+internal inline fun Buffer.commonWriteDecimalLong(v: Long): Buffer {
+ var v = v
+ if (v == 0L) {
+ // Both a shortcut and required since the following code can't handle zero.
+ return writeByte('0'.toInt())
+ }
+
+ var negative = false
+ if (v < 0L) {
+ v = -v
+ if (v < 0L) { // Only true for Long.MIN_VALUE.
+ return writeUtf8("-9223372036854775808")
+ }
+ negative = true
+ }
+
+ // Binary search for character width which favors matching lower numbers.
+ var width =
+ if (v < 100000000L)
+ if (v < 10000L)
+ if (v < 100L)
+ if (v < 10L) 1
+ else 2
+ else if (v < 1000L) 3
+ else 4
+ else if (v < 1000000L)
+ if (v < 100000L) 5
+ else 6
+ else if (v < 10000000L) 7
+ else 8
+ else if (v < 1000000000000L)
+ if (v < 10000000000L)
+ if (v < 1000000000L) 9
+ else 10
+ else if (v < 100000000000L) 11
+ else 12
+ else if (v < 1000000000000000L)
+ if (v < 10000000000000L) 13
+ else if (v < 100000000000000L) 14
+ else 15
+ else if (v < 100000000000000000L)
+ if (v < 10000000000000000L) 16
+ else 17
+ else if (v < 1000000000000000000L) 18
+ else 19
+ if (negative) {
+ ++width
+ }
+
+ val tail = writableSegment(width)
+ val data = tail.data
+ var pos = tail.limit + width // We write backwards from right to left.
+ while (v != 0L) {
+ val digit = (v % 10).toInt()
+ data[--pos] = HEX_DIGIT_BYTES[digit]
+ v /= 10
+ }
+ if (negative) {
+ data[--pos] = '-'.toByte()
+ }
+
+ tail.limit += width
+ this.size += width.toLong()
+ return this
+}
+
+internal inline fun Buffer.commonWriteHexadecimalUnsignedLong(v: Long): Buffer {
+ var v = v
+ if (v == 0L) {
+ // Both a shortcut and required since the following code can't handle zero.
+ return writeByte('0'.toInt())
+ }
+
+ // Mask every bit below the most significant bit to a 1
+ // http://aggregate.org/MAGIC/#Most%20Significant%201%20Bit
+ var x = v
+ x = x or (x ushr 1)
+ x = x or (x ushr 2)
+ x = x or (x ushr 4)
+ x = x or (x ushr 8)
+ x = x or (x ushr 16)
+ x = x or (x ushr 32)
+
+ // Count the number of 1s
+ // http://aggregate.org/MAGIC/#Population%20Count%20(Ones%20Count)
+ x -= x ushr 1 and 0x5555555555555555
+ x = (x ushr 2 and 0x3333333333333333) + (x and 0x3333333333333333)
+ x = (x ushr 4) + x and 0x0f0f0f0f0f0f0f0f
+ x += x ushr 8
+ x += x ushr 16
+ x = (x and 0x3f) + ((x ushr 32) and 0x3f)
+
+ // Round up to the nearest full byte
+ val width = ((x + 3) / 4).toInt()
+
+ val tail = writableSegment(width)
+ val data = tail.data
+ var pos = tail.limit + width - 1
+ val start = tail.limit
+ while (pos >= start) {
+ data[pos] = HEX_DIGIT_BYTES[(v and 0xF).toInt()]
+ v = v ushr 4
+ pos--
+ }
+ tail.limit += width
+ size += width.toLong()
+ return this
+}
+
+internal inline fun Buffer.commonWritableSegment(minimumCapacity: Int): Segment {
+ require(minimumCapacity >= 1 && minimumCapacity <= Segment.SIZE) { "unexpected capacity" }
+
+ if (head == null) {
+ val result = SegmentPool.take() // Acquire a first segment.
+ head = result
+ result.prev = result
+ result.next = result
+ return result
+ }
+
+ var tail = head!!.prev
+ if (tail!!.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
+ tail = tail.push(SegmentPool.take()) // Append a new empty segment to fill up.
+ }
+ return tail
+}
+
+internal inline fun Buffer.commonWrite(source: ByteArray) = write(source, 0, source.size)
+
+internal inline fun Buffer.commonWrite(
+ source: ByteArray,
+ offset: Int,
+ byteCount: Int
+): Buffer {
+ var offset = offset
+ checkOffsetAndCount(source.size.toLong(), offset.toLong(), byteCount.toLong())
+
+ val limit = offset + byteCount
+ while (offset < limit) {
+ val tail = writableSegment(1)
+
+ val toCopy = minOf(limit - offset, Segment.SIZE - tail.limit)
+ source.copyInto(
+ destination = tail.data,
+ destinationOffset = tail.limit,
+ startIndex = offset,
+ endIndex = offset + toCopy
+ )
+
+ offset += toCopy
+ tail.limit += toCopy
+ }
+
+ size += byteCount.toLong()
+ return this
+}
+
+internal inline fun Buffer.commonReadByteArray() = readByteArray(size)
+
+internal inline fun Buffer.commonReadByteArray(byteCount: Long): ByteArray {
+ require(byteCount >= 0 && byteCount <= Int.MAX_VALUE) { "byteCount: $byteCount" }
+ if (size < byteCount) throw EOFException()
+
+ val result = ByteArray(byteCount.toInt())
+ readFully(result)
+ return result
+}
+
+internal inline fun Buffer.commonRead(sink: ByteArray) = read(sink, 0, sink.size)
+
+internal inline fun Buffer.commonReadFully(sink: ByteArray) {
+ var offset = 0
+ while (offset < sink.size) {
+ val read = read(sink, offset, sink.size - offset)
+ if (read == -1) throw EOFException()
+ offset += read
+ }
+}
+
+internal inline fun Buffer.commonRead(sink: ByteArray, offset: Int, byteCount: Int): Int {
+ checkOffsetAndCount(sink.size.toLong(), offset.toLong(), byteCount.toLong())
+
+ val s = head ?: return -1
+ val toCopy = minOf(byteCount, s.limit - s.pos)
+ s.data.copyInto(
+ destination = sink, destinationOffset = offset, startIndex = s.pos, endIndex = s.pos + toCopy
+ )
+
+ s.pos += toCopy
+ size -= toCopy.toLong()
+
+ if (s.pos == s.limit) {
+ head = s.pop()
+ SegmentPool.recycle(s)
+ }
+
+ return toCopy
+}
+
+internal const val OVERFLOW_ZONE = Long.MIN_VALUE / 10L
+internal const val OVERFLOW_DIGIT_START = Long.MIN_VALUE % 10L + 1
+
+internal inline fun Buffer.commonReadDecimalLong(): Long {
+ if (size == 0L) throw EOFException()
+
+ // This value is always built negatively in order to accommodate Long.MIN_VALUE.
+ var value = 0L
+ var seen = 0
+ var negative = false
+ var done = false
+
+ var overflowDigit = OVERFLOW_DIGIT_START
+
+ do {
+ val segment = head!!
+
+ val data = segment.data
+ var pos = segment.pos
+ val limit = segment.limit
+
+ while (pos < limit) {
+ val b = data[pos]
+ if (b >= '0'.toByte() && b <= '9'.toByte()) {
+ val digit = '0'.toByte() - b
+
+ // Detect when the digit would cause an overflow.
+ if (value < OVERFLOW_ZONE || value == OVERFLOW_ZONE && digit < overflowDigit) {
+ val buffer = Buffer().writeDecimalLong(value).writeByte(b.toInt())
+ if (!negative) buffer.readByte() // Skip negative sign.
+ throw NumberFormatException("Number too large: ${buffer.readUtf8()}")
+ }
+ value *= 10L
+ value += digit.toLong()
+ } else if (b == '-'.toByte() && seen == 0) {
+ negative = true
+ overflowDigit -= 1
+ } else {
+ if (seen == 0) {
+ throw NumberFormatException(
+ "Expected leading [0-9] or '-' character but was 0x${b.toHexString()}"
+ )
+ }
+ // Set a flag to stop iteration. We still need to run through segment updating below.
+ done = true
+ break
+ }
+ pos++
+ seen++
+ }
+
+ if (pos == limit) {
+ head = segment.pop()
+ SegmentPool.recycle(segment)
+ } else {
+ segment.pos = pos
+ }
+ } while (!done && head != null)
+
+ size -= seen.toLong()
+ return if (negative) value else -value
+}
+
+internal inline fun Buffer.commonReadHexadecimalUnsignedLong(): Long {
+ if (size == 0L) throw EOFException()
+
+ var value = 0L
+ var seen = 0
+ var done = false
+
+ do {
+ val segment = head!!
+
+ val data = segment.data
+ var pos = segment.pos
+ val limit = segment.limit
+
+ while (pos < limit) {
+ val digit: Int
+
+ val b = data[pos]
+ if (b >= '0'.toByte() && b <= '9'.toByte()) {
+ digit = b - '0'.toByte()
+ } else if (b >= 'a'.toByte() && b <= 'f'.toByte()) {
+ digit = b - 'a'.toByte() + 10
+ } else if (b >= 'A'.toByte() && b <= 'F'.toByte()) {
+ digit = b - 'A'.toByte() + 10 // We never write uppercase, but we support reading it.
+ } else {
+ if (seen == 0) {
+ throw NumberFormatException(
+ "Expected leading [0-9a-fA-F] character but was 0x${b.toHexString()}"
+ )
+ }
+ // Set a flag to stop iteration. We still need to run through segment updating below.
+ done = true
+ break
+ }
+
+ // Detect when the shift will overflow.
+ if (value and -0x1000000000000000L != 0L) {
+ val buffer = Buffer().writeHexadecimalUnsignedLong(value).writeByte(b.toInt())
+ throw NumberFormatException("Number too large: " + buffer.readUtf8())
+ }
+
+ value = value shl 4
+ value = value or digit.toLong()
+ pos++
+ seen++
+ }
+
+ if (pos == limit) {
+ head = segment.pop()
+ SegmentPool.recycle(segment)
+ } else {
+ segment.pos = pos
+ }
+ } while (!done && head != null)
+
+ size -= seen.toLong()
+ return value
+}
+
+internal inline fun Buffer.commonReadByteString(): ByteString = readByteString(size)
+
+internal inline fun Buffer.commonReadByteString(byteCount: Long): ByteString {
+ require(byteCount >= 0 && byteCount <= Int.MAX_VALUE) { "byteCount: $byteCount" }
+ if (size < byteCount) throw EOFException()
+
+ if (byteCount >= SEGMENTING_THRESHOLD) {
+ return snapshot(byteCount.toInt()).also { skip(byteCount) }
+ } else {
+ return ByteString(readByteArray(byteCount))
+ }
+}
+
+internal inline fun Buffer.commonSelect(options: Options): Int {
+ val index = selectPrefix(options)
+ if (index == -1) return -1
+
+ // If the prefix match actually matched a full byte string, consume it and return it.
+ val selectedSize = options.byteStrings[index].size
+ skip(selectedSize.toLong())
+ return index
+}
+
+internal inline fun Buffer.commonReadFully(sink: Buffer, byteCount: Long) {
+ if (size < byteCount) {
+ sink.write(this, size) // Exhaust ourselves.
+ throw EOFException()
+ }
+ sink.write(this, byteCount)
+}
+
+internal inline fun Buffer.commonReadAll(sink: Sink): Long {
+ val byteCount = size
+ if (byteCount > 0L) {
+ sink.write(this, byteCount)
+ }
+ return byteCount
+}
+
+internal inline fun Buffer.commonReadUtf8(byteCount: Long): String {
+ require(byteCount >= 0 && byteCount <= Int.MAX_VALUE) { "byteCount: $byteCount" }
+ if (size < byteCount) throw EOFException()
+ if (byteCount == 0L) return ""
+
+ val s = head!!
+ if (s.pos + byteCount > s.limit) {
+ // If the string spans multiple segments, delegate to readBytes().
+
+ return readByteArray(byteCount).commonToUtf8String()
+ }
+
+ val result = s.data.commonToUtf8String(s.pos, s.pos + byteCount.toInt())
+ s.pos += byteCount.toInt()
+ size -= byteCount
+
+ if (s.pos == s.limit) {
+ head = s.pop()
+ SegmentPool.recycle(s)
+ }
+
+ return result
+}
+
+internal inline fun Buffer.commonReadUtf8Line(): String? {
+ val newline = indexOf('\n'.toByte())
+
+ return when {
+ newline != -1L -> readUtf8Line(newline)
+ size != 0L -> readUtf8(size)
+ else -> null
+ }
+}
+
+internal inline fun Buffer.commonReadUtf8LineStrict(limit: Long): String {
+ require(limit >= 0L) { "limit < 0: $limit" }
+ val scanLength = if (limit == Long.MAX_VALUE) Long.MAX_VALUE else limit + 1L
+ val newline = indexOf('\n'.toByte(), 0L, scanLength)
+ if (newline != -1L) return readUtf8Line(newline)
+ if (scanLength < size &&
+ this[scanLength - 1] == '\r'.toByte() &&
+ this[scanLength] == '\n'.toByte()
+ ) {
+ return readUtf8Line(scanLength) // The line was 'limit' UTF-8 bytes followed by \r\n.
+ }
+ val data = Buffer()
+ copyTo(data, 0, minOf(32, size))
+ throw EOFException(
+ "\\n not found: limit=${minOf(
+ size,
+ limit
+ )} content=${data.readByteString().hex()}${'…'}"
+ )
+}
+
+internal inline fun Buffer.commonReadUtf8CodePoint(): Int {
+ if (size == 0L) throw EOFException()
+
+ val b0 = this[0]
+ var codePoint: Int
+ val byteCount: Int
+ val min: Int
+
+ when {
+ b0 and 0x80 == 0 -> {
+ // 0xxxxxxx.
+ codePoint = b0 and 0x7f
+ byteCount = 1 // 7 bits (ASCII).
+ min = 0x0
+ }
+ b0 and 0xe0 == 0xc0 -> {
+ // 0x110xxxxx
+ codePoint = b0 and 0x1f
+ byteCount = 2 // 11 bits (5 + 6).
+ min = 0x80
+ }
+ b0 and 0xf0 == 0xe0 -> {
+ // 0x1110xxxx
+ codePoint = b0 and 0x0f
+ byteCount = 3 // 16 bits (4 + 6 + 6).
+ min = 0x800
+ }
+ b0 and 0xf8 == 0xf0 -> {
+ // 0x11110xxx
+ codePoint = b0 and 0x07
+ byteCount = 4 // 21 bits (3 + 6 + 6 + 6).
+ min = 0x10000
+ }
+ else -> {
+ // We expected the first byte of a code point but got something else.
+ skip(1)
+ return REPLACEMENT_CODE_POINT
+ }
+ }
+
+ if (size < byteCount) {
+ throw EOFException("size < $byteCount: $size (to read code point prefixed 0x${b0.toHexString()})")
+ }
+
+ // Read the continuation bytes. If we encounter a non-continuation byte, the sequence consumed
+ // thus far is truncated and is decoded as the replacement character. That non-continuation byte
+ // is left in the stream for processing by the next call to readUtf8CodePoint().
+ for (i in 1 until byteCount) {
+ val b = this[i.toLong()]
+ if (b and 0xc0 == 0x80) {
+ // 0x10xxxxxx
+ codePoint = codePoint shl 6
+ codePoint = codePoint or (b and 0x3f)
+ } else {
+ skip(i.toLong())
+ return REPLACEMENT_CODE_POINT
+ }
+ }
+
+ skip(byteCount.toLong())
+
+ return when {
+ codePoint > 0x10ffff -> {
+ REPLACEMENT_CODE_POINT // Reject code points larger than the Unicode maximum.
+ }
+ codePoint in 0xd800..0xdfff -> {
+ REPLACEMENT_CODE_POINT // Reject partial surrogates.
+ }
+ codePoint < min -> {
+ REPLACEMENT_CODE_POINT // Reject overlong code points.
+ }
+ else -> codePoint
+ }
+}
+
+internal inline fun Buffer.commonWriteUtf8(string: String, beginIndex: Int, endIndex: Int): Buffer {
+ require(beginIndex >= 0) { "beginIndex < 0: $beginIndex" }
+ require(endIndex >= beginIndex) { "endIndex < beginIndex: $endIndex < $beginIndex" }
+ require(endIndex <= string.length) { "endIndex > string.length: $endIndex > ${string.length}" }
+
+ // Transcode a UTF-16 Java String to UTF-8 bytes.
+ var i = beginIndex
+ while (i < endIndex) {
+ var c = string[i].toInt()
+
+ when {
+ c < 0x80 -> {
+ val tail = writableSegment(1)
+ val data = tail.data
+ val segmentOffset = tail.limit - i
+ val runLimit = minOf(endIndex, Segment.SIZE - segmentOffset)
+
+ // Emit a 7-bit character with 1 byte.
+ data[segmentOffset + i++] = c.toByte() // 0xxxxxxx
+
+ // Fast-path contiguous runs of ASCII characters. This is ugly, but yields a ~4x performance
+ // improvement over independent calls to writeByte().
+ while (i < runLimit) {
+ c = string[i].toInt()
+ if (c >= 0x80) break
+ data[segmentOffset + i++] = c.toByte() // 0xxxxxxx
+ }
+
+ val runSize = i + segmentOffset - tail.limit // Equivalent to i - (previous i).
+ tail.limit += runSize
+ size += runSize.toLong()
+ }
+
+ c < 0x800 -> {
+ // Emit a 11-bit character with 2 bytes.
+ val tail = writableSegment(2)
+ /* ktlint-disable no-multi-spaces */
+ tail.data[tail.limit ] = (c shr 6 or 0xc0).toByte() // 110xxxxx
+ tail.data[tail.limit + 1] = (c and 0x3f or 0x80).toByte() // 10xxxxxx
+ /* ktlint-enable no-multi-spaces */
+ tail.limit += 2
+ size += 2L
+ i++
+ }
+
+ c < 0xd800 || c > 0xdfff -> {
+ // Emit a 16-bit character with 3 bytes.
+ val tail = writableSegment(3)
+ /* ktlint-disable no-multi-spaces */
+ tail.data[tail.limit ] = (c shr 12 or 0xe0).toByte() // 1110xxxx
+ tail.data[tail.limit + 1] = (c shr 6 and 0x3f or 0x80).toByte() // 10xxxxxx
+ tail.data[tail.limit + 2] = (c and 0x3f or 0x80).toByte() // 10xxxxxx
+ /* ktlint-enable no-multi-spaces */
+ tail.limit += 3
+ size += 3L
+ i++
+ }
+
+ else -> {
+ // c is a surrogate. Make sure it is a high surrogate & that its successor is a low
+ // surrogate. If not, the UTF-16 is invalid, in which case we emit a replacement
+ // character.
+ val low = (if (i + 1 < endIndex) string[i + 1].toInt() else 0)
+ if (c > 0xdbff || low !in 0xdc00..0xdfff) {
+ writeByte('?'.toInt())
+ i++
+ } else {
+ // UTF-16 high surrogate: 110110xxxxxxxxxx (10 bits)
+ // UTF-16 low surrogate: 110111yyyyyyyyyy (10 bits)
+ // Unicode code point: 00010000000000000000 + xxxxxxxxxxyyyyyyyyyy (21 bits)
+ val codePoint = 0x010000 + (c and 0x03ff shl 10 or (low and 0x03ff))
+
+ // Emit a 21-bit character with 4 bytes.
+ val tail = writableSegment(4)
+ /* ktlint-disable no-multi-spaces */
+ tail.data[tail.limit ] = (codePoint shr 18 or 0xf0).toByte() // 11110xxx
+ tail.data[tail.limit + 1] = (codePoint shr 12 and 0x3f or 0x80).toByte() // 10xxxxxx
+ tail.data[tail.limit + 2] = (codePoint shr 6 and 0x3f or 0x80).toByte() // 10xxyyyy
+ tail.data[tail.limit + 3] = (codePoint and 0x3f or 0x80).toByte() // 10yyyyyy
+ /* ktlint-enable no-multi-spaces */
+ tail.limit += 4
+ size += 4L
+ i += 2
+ }
+ }
+ }
+ }
+
+ return this
+}
+
+internal inline fun Buffer.commonWriteUtf8CodePoint(codePoint: Int): Buffer {
+ when {
+ codePoint < 0x80 -> {
+ // Emit a 7-bit code point with 1 byte.
+ writeByte(codePoint)
+ }
+ codePoint < 0x800 -> {
+ // Emit a 11-bit code point with 2 bytes.
+ val tail = writableSegment(2)
+ /* ktlint-disable no-multi-spaces */
+ tail.data[tail.limit ] = (codePoint shr 6 or 0xc0).toByte() // 110xxxxx
+ tail.data[tail.limit + 1] = (codePoint and 0x3f or 0x80).toByte() // 10xxxxxx
+ /* ktlint-enable no-multi-spaces */
+ tail.limit += 2
+ size += 2L
+ }
+ codePoint in 0xd800..0xdfff -> {
+ // Emit a replacement character for a partial surrogate.
+ writeByte('?'.toInt())
+ }
+ codePoint < 0x10000 -> {
+ // Emit a 16-bit code point with 3 bytes.
+ val tail = writableSegment(3)
+ /* ktlint-disable no-multi-spaces */
+ tail.data[tail.limit ] = (codePoint shr 12 or 0xe0).toByte() // 1110xxxx
+ tail.data[tail.limit + 1] = (codePoint shr 6 and 0x3f or 0x80).toByte() // 10xxxxxx
+ tail.data[tail.limit + 2] = (codePoint and 0x3f or 0x80).toByte() // 10xxxxxx
+ /* ktlint-enable no-multi-spaces */
+ tail.limit += 3
+ size += 3L
+ }
+ codePoint <= 0x10ffff -> {
+ // Emit a 21-bit code point with 4 bytes.
+ val tail = writableSegment(4)
+ /* ktlint-disable no-multi-spaces */
+ tail.data[tail.limit ] = (codePoint shr 18 or 0xf0).toByte() // 11110xxx
+ tail.data[tail.limit + 1] = (codePoint shr 12 and 0x3f or 0x80).toByte() // 10xxxxxx
+ tail.data[tail.limit + 2] = (codePoint shr 6 and 0x3f or 0x80).toByte() // 10xxyyyy
+ tail.data[tail.limit + 3] = (codePoint and 0x3f or 0x80).toByte() // 10yyyyyy
+ /* ktlint-enable no-multi-spaces */
+ tail.limit += 4
+ size += 4L
+ }
+ else -> {
+ throw IllegalArgumentException("Unexpected code point: 0x${codePoint.toHexString()}")
+ }
+ }
+
+ return this
+}
+
+internal inline fun Buffer.commonWriteAll(source: Source): Long {
+ var totalBytesRead = 0L
+ while (true) {
+ val readCount = source.read(this, Segment.SIZE.toLong())
+ if (readCount == -1L) break
+ totalBytesRead += readCount
+ }
+ return totalBytesRead
+}
+
+internal inline fun Buffer.commonWrite(source: Source, byteCount: Long): Buffer {
+ var byteCount = byteCount
+ while (byteCount > 0L) {
+ val read = source.read(this, byteCount)
+ if (read == -1L) throw EOFException()
+ byteCount -= read
+ }
+ return this
+}
+
+internal inline fun Buffer.commonWriteByte(b: Int): Buffer {
+ val tail = writableSegment(1)
+ tail.data[tail.limit++] = b.toByte()
+ size += 1L
+ return this
+}
+
+internal inline fun Buffer.commonWriteShort(s: Int): Buffer {
+ val tail = writableSegment(2)
+ val data = tail.data
+ var limit = tail.limit
+ data[limit++] = (s ushr 8 and 0xff).toByte()
+ data[limit++] = (s and 0xff).toByte() // ktlint-disable no-multi-spaces
+ tail.limit = limit
+ size += 2L
+ return this
+}
+
+internal inline fun Buffer.commonWriteInt(i: Int): Buffer {
+ val tail = writableSegment(4)
+ val data = tail.data
+ var limit = tail.limit
+ data[limit++] = (i ushr 24 and 0xff).toByte()
+ data[limit++] = (i ushr 16 and 0xff).toByte()
+ data[limit++] = (i ushr 8 and 0xff).toByte() // ktlint-disable no-multi-spaces
+ data[limit++] = (i and 0xff).toByte() // ktlint-disable no-multi-spaces
+ tail.limit = limit
+ size += 4L
+ return this
+}
+
+internal inline fun Buffer.commonWriteLong(v: Long): Buffer {
+ val tail = writableSegment(8)
+ val data = tail.data
+ var limit = tail.limit
+ data[limit++] = (v ushr 56 and 0xffL).toByte()
+ data[limit++] = (v ushr 48 and 0xffL).toByte()
+ data[limit++] = (v ushr 40 and 0xffL).toByte()
+ data[limit++] = (v ushr 32 and 0xffL).toByte()
+ data[limit++] = (v ushr 24 and 0xffL).toByte()
+ data[limit++] = (v ushr 16 and 0xffL).toByte()
+ data[limit++] = (v ushr 8 and 0xffL).toByte() // ktlint-disable no-multi-spaces
+ data[limit++] = (v and 0xffL).toByte() // ktlint-disable no-multi-spaces
+ tail.limit = limit
+ size += 8L
+ return this
+}
+
+internal inline fun Buffer.commonWrite(source: Buffer, byteCount: Long) {
+ var byteCount = byteCount
+ // Move bytes from the head of the source buffer to the tail of this buffer
+ // while balancing two conflicting goals: don't waste CPU and don't waste
+ // memory.
+ //
+ //
+ // Don't waste CPU (ie. don't copy data around).
+ //
+ // Copying large amounts of data is expensive. Instead, we prefer to
+ // reassign entire segments from one buffer to the other.
+ //
+ //
+ // Don't waste memory.
+ //
+ // As an invariant, adjacent pairs of segments in a buffer should be at
+ // least 50% full, except for the head segment and the tail segment.
+ //
+ // The head segment cannot maintain the invariant because the application is
+ // consuming bytes from this segment, decreasing its level.
+ //
+ // The tail segment cannot maintain the invariant because the application is
+ // producing bytes, which may require new nearly-empty tail segments to be
+ // appended.
+ //
+ //
+ // Moving segments between buffers
+ //
+ // When writing one buffer to another, we prefer to reassign entire segments
+ // over copying bytes into their most compact form. Suppose we have a buffer
+ // with these segment levels [91%, 61%]. If we append a buffer with a
+ // single [72%] segment, that yields [91%, 61%, 72%]. No bytes are copied.
+ //
+ // Or suppose we have a buffer with these segment levels: [100%, 2%], and we
+ // want to append it to a buffer with these segment levels [99%, 3%]. This
+ // operation will yield the following segments: [100%, 2%, 99%, 3%]. That
+ // is, we do not spend time copying bytes around to achieve more efficient
+ // memory use like [100%, 100%, 4%].
+ //
+ // When combining buffers, we will compact adjacent buffers when their
+ // combined level doesn't exceed 100%. For example, when we start with
+ // [100%, 40%] and append [30%, 80%], the result is [100%, 70%, 80%].
+ //
+ //
+ // Splitting segments
+ //
+ // Occasionally we write only part of a source buffer to a sink buffer. For
+ // example, given a sink [51%, 91%], we may want to write the first 30% of
+ // a source [92%, 82%] to it. To simplify, we first transform the source to
+ // an equivalent buffer [30%, 62%, 82%] and then move the head segment,
+ // yielding sink [51%, 91%, 30%] and source [62%, 82%].
+
+ require(source !== this) { "source == this" }
+ checkOffsetAndCount(source.size, 0, byteCount)
+
+ while (byteCount > 0L) {
+ // Is a prefix of the source's head segment all that we need to move?
+ if (byteCount < source.head!!.limit - source.head!!.pos) {
+ val tail = if (head != null) head!!.prev else null
+ if (tail != null && tail.owner &&
+ byteCount + tail.limit - (if (tail.shared) 0 else tail.pos) <= Segment.SIZE
+ ) {
+ // Our existing segments are sufficient. Move bytes from source's head to our tail.
+ source.head!!.writeTo(tail, byteCount.toInt())
+ source.size -= byteCount
+ size += byteCount
+ return
+ } else {
+ // We're going to need another segment. Split the source's head
+ // segment in two, then move the first of those two to this buffer.
+ source.head = source.head!!.split(byteCount.toInt())
+ }
+ }
+
+ // Remove the source's head segment and append it to our tail.
+ val segmentToMove = source.head
+ val movedByteCount = (segmentToMove!!.limit - segmentToMove.pos).toLong()
+ source.head = segmentToMove.pop()
+ if (head == null) {
+ head = segmentToMove
+ segmentToMove.prev = segmentToMove
+ segmentToMove.next = segmentToMove.prev
+ } else {
+ var tail = head!!.prev
+ tail = tail!!.push(segmentToMove)
+ tail.compact()
+ }
+ source.size -= movedByteCount
+ size += movedByteCount
+ byteCount -= movedByteCount
+ }
+}
+
+internal inline fun Buffer.commonRead(sink: Buffer, byteCount: Long): Long {
+ var byteCount = byteCount
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ if (size == 0L) return -1L
+ if (byteCount > size) byteCount = size
+ sink.write(this, byteCount)
+ return byteCount
+}
+
+internal inline fun Buffer.commonIndexOf(b: Byte, fromIndex: Long, toIndex: Long): Long {
+ var fromIndex = fromIndex
+ var toIndex = toIndex
+ require(fromIndex in 0..toIndex) { "size=$size fromIndex=$fromIndex toIndex=$toIndex" }
+
+ if (toIndex > size) toIndex = size
+ if (fromIndex == toIndex) return -1L
+
+ seek(fromIndex) { s, offset ->
+ var s = s ?: return -1L
+ var offset = offset
+
+ // Scan through the segments, searching for b.
+ while (offset < toIndex) {
+ val data = s.data
+ val limit = minOf(s.limit.toLong(), s.pos + toIndex - offset).toInt()
+ var pos = (s.pos + fromIndex - offset).toInt()
+ while (pos < limit) {
+ if (data[pos] == b) {
+ return pos - s.pos + offset
+ }
+ pos++
+ }
+
+ // Not in this segment. Try the next one.
+ offset += (s.limit - s.pos).toLong()
+ fromIndex = offset
+ s = s.next!!
+ }
+
+ return -1L
+ }
+}
+
+internal inline fun Buffer.commonIndexOf(bytes: ByteString, fromIndex: Long): Long {
+ var fromIndex = fromIndex
+ require(bytes.size > 0) { "bytes is empty" }
+ require(fromIndex >= 0L) { "fromIndex < 0: $fromIndex" }
+
+ seek(fromIndex) { s, offset ->
+ var s = s ?: return -1L
+ var offset = offset
+
+ // Scan through the segments, searching for the lead byte. Each time that is found, delegate
+ // to rangeEquals() to check for a complete match.
+ val targetByteArray = bytes.internalArray()
+ val b0 = targetByteArray[0]
+ val bytesSize = bytes.size
+ val resultLimit = size - bytesSize + 1L
+ while (offset < resultLimit) {
+ // Scan through the current segment.
+ val data = s.data
+ val segmentLimit = okio.minOf(s.limit, s.pos + resultLimit - offset).toInt()
+ for (pos in (s.pos + fromIndex - offset).toInt() until segmentLimit) {
+ if (data[pos] == b0 && rangeEquals(s, pos + 1, targetByteArray, 1, bytesSize)) {
+ return pos - s.pos + offset
+ }
+ }
+
+ // Not in this segment. Try the next one.
+ offset += (s.limit - s.pos).toLong()
+ fromIndex = offset
+ s = s.next!!
+ }
+
+ return -1L
+ }
+}
+
+internal inline fun Buffer.commonIndexOfElement(targetBytes: ByteString, fromIndex: Long): Long {
+ var fromIndex = fromIndex
+ require(fromIndex >= 0L) { "fromIndex < 0: $fromIndex" }
+
+ seek(fromIndex) { s, offset ->
+ var s = s ?: return -1L
+ var offset = offset
+
+ // Special case searching for one of two bytes. This is a common case for tools like Moshi,
+ // which search for pairs of chars like `\r` and `\n` or {@code `"` and `\`. The impact of this
+ // optimization is a ~5x speedup for this case without a substantial cost to other cases.
+ if (targetBytes.size == 2) {
+ // Scan through the segments, searching for either of the two bytes.
+ val b0 = targetBytes[0]
+ val b1 = targetBytes[1]
+ while (offset < size) {
+ val data = s.data
+ var pos = (s.pos + fromIndex - offset).toInt()
+ val limit = s.limit
+ while (pos < limit) {
+ val b = data[pos].toInt()
+ if (b == b0.toInt() || b == b1.toInt()) {
+ return pos - s.pos + offset
+ }
+ pos++
+ }
+
+ // Not in this segment. Try the next one.
+ offset += (s.limit - s.pos).toLong()
+ fromIndex = offset
+ s = s.next!!
+ }
+ } else {
+ // Scan through the segments, searching for a byte that's also in the array.
+ val targetByteArray = targetBytes.internalArray()
+ while (offset < size) {
+ val data = s.data
+ var pos = (s.pos + fromIndex - offset).toInt()
+ val limit = s.limit
+ while (pos < limit) {
+ val b = data[pos].toInt()
+ for (t in targetByteArray) {
+ if (b == t.toInt()) return pos - s.pos + offset
+ }
+ pos++
+ }
+
+ // Not in this segment. Try the next one.
+ offset += (s.limit - s.pos).toLong()
+ fromIndex = offset
+ s = s.next!!
+ }
+ }
+
+ return -1L
+ }
+}
+
+internal inline fun Buffer.commonRangeEquals(
+ offset: Long,
+ bytes: ByteString,
+ bytesOffset: Int,
+ byteCount: Int
+): Boolean {
+ if (offset < 0L ||
+ bytesOffset < 0 ||
+ byteCount < 0 ||
+ size - offset < byteCount ||
+ bytes.size - bytesOffset < byteCount
+ ) {
+ return false
+ }
+ for (i in 0 until byteCount) {
+ if (this[offset + i] != bytes[bytesOffset + i]) {
+ return false
+ }
+ }
+ return true
+}
+
+internal inline fun Buffer.commonEquals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Buffer) return false
+ if (size != other.size) return false
+ if (size == 0L) return true // Both buffers are empty.
+
+ var sa = this.head!!
+ var sb = other.head!!
+ var posA = sa.pos
+ var posB = sb.pos
+
+ var pos = 0L
+ var count: Long
+ while (pos < size) {
+ count = minOf(sa.limit - posA, sb.limit - posB).toLong()
+
+ for (i in 0L until count) {
+ if (sa.data[posA++] != sb.data[posB++]) return false
+ }
+
+ if (posA == sa.limit) {
+ sa = sa.next!!
+ posA = sa.pos
+ }
+
+ if (posB == sb.limit) {
+ sb = sb.next!!
+ posB = sb.pos
+ }
+ pos += count
+ }
+
+ return true
+}
+
+internal inline fun Buffer.commonHashCode(): Int {
+ var s = head ?: return 0
+ var result = 1
+ do {
+ var pos = s.pos
+ val limit = s.limit
+ while (pos < limit) {
+ result = 31 * result + s.data[pos]
+ pos++
+ }
+ s = s.next!!
+ } while (s !== head)
+ return result
+}
+
+internal inline fun Buffer.commonCopy(): Buffer {
+ val result = Buffer()
+ if (size == 0L) return result
+
+ val head = head!!
+ val headCopy = head.sharedCopy()
+
+ result.head = headCopy
+ headCopy.prev = result.head
+ headCopy.next = headCopy.prev
+
+ var s = head.next
+ while (s !== head) {
+ headCopy.prev!!.push(s!!.sharedCopy())
+ s = s.next
+ }
+
+ result.size = size
+ return result
+}
+
+/** Returns an immutable copy of this buffer as a byte string. */
+internal inline fun Buffer.commonSnapshot(): ByteString {
+ check(size <= Int.MAX_VALUE) { "size > Int.MAX_VALUE: $size" }
+ return snapshot(size.toInt())
+}
+
+/** Returns an immutable copy of the first `byteCount` bytes of this buffer as a byte string. */
+internal inline fun Buffer.commonSnapshot(byteCount: Int): ByteString {
+ if (byteCount == 0) return ByteString.EMPTY
+ checkOffsetAndCount(size, 0, byteCount.toLong())
+
+ // Walk through the buffer to count how many segments we'll need.
+ var offset = 0
+ var segmentCount = 0
+ var s = head
+ while (offset < byteCount) {
+ if (s!!.limit == s.pos) {
+ throw AssertionError("s.limit == s.pos") // Empty segment. This should not happen!
+ }
+ offset += s.limit - s.pos
+ segmentCount++
+ s = s.next
+ }
+
+ // Walk through the buffer again to assign segments and build the directory.
+ val segments = arrayOfNulls<ByteArray?>(segmentCount)
+ val directory = IntArray(segmentCount * 2)
+ offset = 0
+ segmentCount = 0
+ s = head
+ while (offset < byteCount) {
+ segments[segmentCount] = s!!.data
+ offset += s.limit - s.pos
+ // Despite sharing more bytes, only report having up to byteCount.
+ directory[segmentCount] = minOf(offset, byteCount)
+ directory[segmentCount + segments.size] = s.pos
+ s.shared = true
+ segmentCount++
+ s = s.next
+ }
+ @Suppress("UNCHECKED_CAST")
+ return SegmentedByteString(segments as Array<ByteArray>, directory)
+}
+
+internal fun Buffer.commonReadUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor {
+ check(unsafeCursor.buffer == null) { "already attached to a buffer" }
+
+ unsafeCursor.buffer = this
+ unsafeCursor.readWrite = false
+ return unsafeCursor
+}
+
+internal fun Buffer.commonReadAndWriteUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor {
+ check(unsafeCursor.buffer == null) { "already attached to a buffer" }
+
+ unsafeCursor.buffer = this
+ unsafeCursor.readWrite = true
+ return unsafeCursor
+}
+
+internal inline fun UnsafeCursor.commonNext(): Int {
+ check(offset != buffer!!.size) { "no more bytes" }
+ return if (offset == -1L) seek(0L) else seek(offset + (end - start))
+}
+
+internal inline fun UnsafeCursor.commonSeek(offset: Long): Int {
+ val buffer = checkNotNull(buffer) { "not attached to a buffer" }
+ if (offset < -1 || offset > buffer.size) {
+ throw ArrayIndexOutOfBoundsException("offset=$offset > size=${buffer.size}")
+ }
+
+ if (offset == -1L || offset == buffer.size) {
+ this.segment = null
+ this.offset = offset
+ this.data = null
+ this.start = -1
+ this.end = -1
+ return -1
+ }
+
+ // Navigate to the segment that contains `offset`. Start from our current segment if possible.
+ var min = 0L
+ var max = buffer.size
+ var head = buffer.head
+ var tail = buffer.head
+ if (this.segment != null) {
+ val segmentOffset = this.offset - (this.start - this.segment!!.pos)
+ if (segmentOffset > offset) {
+ // Set the cursor segment to be the 'end'
+ max = segmentOffset
+ tail = this.segment
+ } else {
+ // Set the cursor segment to be the 'beginning'
+ min = segmentOffset
+ head = this.segment
+ }
+ }
+
+ var next: Segment?
+ var nextOffset: Long
+ if (max - offset > offset - min) {
+ // Start at the 'beginning' and search forwards
+ next = head
+ nextOffset = min
+ while (offset >= nextOffset + (next!!.limit - next.pos)) {
+ nextOffset += (next.limit - next.pos).toLong()
+ next = next.next
+ }
+ } else {
+ // Start at the 'end' and search backwards
+ next = tail
+ nextOffset = max
+ while (nextOffset > offset) {
+ next = next!!.prev
+ nextOffset -= (next!!.limit - next.pos).toLong()
+ }
+ }
+
+ // If we're going to write and our segment is shared, swap it for a read-write one.
+ if (readWrite && next!!.shared) {
+ val unsharedNext = next.unsharedCopy()
+ if (buffer.head === next) {
+ buffer.head = unsharedNext
+ }
+ next = next.push(unsharedNext)
+ next.prev!!.pop()
+ }
+
+ // Update this cursor to the requested offset within the found segment.
+ this.segment = next
+ this.offset = offset
+ this.data = next!!.data
+ this.start = next.pos + (offset - nextOffset).toInt()
+ this.end = next.limit
+ return end - start
+}
+
+internal inline fun UnsafeCursor.commonResizeBuffer(newSize: Long): Long {
+ val buffer = checkNotNull(buffer) { "not attached to a buffer" }
+ check(readWrite) { "resizeBuffer() only permitted for read/write buffers" }
+
+ val oldSize = buffer.size
+ if (newSize <= oldSize) {
+ require(newSize >= 0L) { "newSize < 0: $newSize" }
+ // Shrink the buffer by either shrinking segments or removing them.
+ var bytesToSubtract = oldSize - newSize
+ while (bytesToSubtract > 0L) {
+ val tail = buffer.head!!.prev
+ val tailSize = tail!!.limit - tail.pos
+ if (tailSize <= bytesToSubtract) {
+ buffer.head = tail.pop()
+ okio.SegmentPool.recycle(tail)
+ bytesToSubtract -= tailSize.toLong()
+ } else {
+ tail.limit -= bytesToSubtract.toInt()
+ break
+ }
+ }
+ // Seek to the end.
+ this.segment = null
+ this.offset = newSize
+ this.data = null
+ this.start = -1
+ this.end = -1
+ } else if (newSize > oldSize) {
+ // Enlarge the buffer by either enlarging segments or adding them.
+ var needsToSeek = true
+ var bytesToAdd = newSize - oldSize
+ while (bytesToAdd > 0L) {
+ val tail = buffer.writableSegment(1)
+ val segmentBytesToAdd = minOf(bytesToAdd, Segment.SIZE - tail.limit).toInt()
+ tail.limit += segmentBytesToAdd
+ bytesToAdd -= segmentBytesToAdd.toLong()
+
+ // If this is the first segment we're adding, seek to it.
+ if (needsToSeek) {
+ this.segment = tail
+ this.offset = oldSize
+ this.data = tail.data
+ this.start = tail.limit - segmentBytesToAdd
+ this.end = tail.limit
+ needsToSeek = false
+ }
+ }
+ }
+
+ buffer.size = newSize
+
+ return oldSize
+}
+
+internal inline fun UnsafeCursor.commonExpandBuffer(minByteCount: Int): Long {
+ require(minByteCount > 0) { "minByteCount <= 0: $minByteCount" }
+ require(minByteCount <= Segment.SIZE) { "minByteCount > Segment.SIZE: $minByteCount" }
+ val buffer = checkNotNull(buffer) { "not attached to a buffer" }
+ check(readWrite) { "expandBuffer() only permitted for read/write buffers" }
+
+ val oldSize = buffer.size
+ val tail = buffer.writableSegment(minByteCount)
+ val result = Segment.SIZE - tail.limit
+ tail.limit = Segment.SIZE
+ buffer.size = oldSize + result
+
+ // Seek to the old size.
+ this.segment = tail
+ this.offset = oldSize
+ this.data = tail.data
+ this.start = Segment.SIZE - result
+ this.end = Segment.SIZE
+
+ return result.toLong()
+}
+
+internal inline fun UnsafeCursor.commonClose() {
+ // TODO(jwilson): use edit counts or other information to track unexpected changes?
+ check(buffer != null) { "not attached to a buffer" }
+
+ buffer = null
+ segment = null
+ offset = -1L
+ data = null
+ start = -1
+ end = -1
+}
diff --git a/okio/src/commonMain/kotlin/okio/internal/ByteString.kt b/okio/src/commonMain/kotlin/okio/internal/ByteString.kt
new file mode 100644
index 00000000..7a1a488b
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/internal/ByteString.kt
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio.internal
+
+import okio.BASE64_URL_SAFE
+import okio.Buffer
+import okio.ByteString
+import okio.REPLACEMENT_CODE_POINT
+import okio.and
+import okio.arrayRangeEquals
+import okio.asUtf8ToByteArray
+import okio.checkOffsetAndCount
+import okio.decodeBase64ToArray
+import okio.encodeBase64
+import okio.isIsoControl
+import okio.processUtf8CodePoints
+import okio.shr
+import okio.toUtf8String
+
+// TODO Kotlin's expect classes can't have default implementations, so platform implementations
+// have to call these functions. Remove all this nonsense when expect class allow actual code.
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonUtf8(): String {
+ var result = utf8
+ if (result == null) {
+ // We don't care if we double-allocate in racy code.
+ result = internalArray().toUtf8String()
+ utf8 = result
+ }
+ return result
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonBase64(): String = data.encodeBase64()
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonBase64Url() = data.encodeBase64(map = BASE64_URL_SAFE)
+
+internal val HEX_DIGIT_CHARS =
+ charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonHex(): String {
+ val result = CharArray(data.size * 2)
+ var c = 0
+ for (b in data) {
+ result[c++] = HEX_DIGIT_CHARS[b shr 4 and 0xf]
+ result[c++] = HEX_DIGIT_CHARS[b and 0xf] // ktlint-disable no-multi-spaces
+ }
+ return String(result)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonToAsciiLowercase(): ByteString {
+ // Search for an uppercase character. If we don't find one, return this.
+ var i = 0
+ while (i < data.size) {
+ var c = data[i]
+ if (c < 'A'.toByte() || c > 'Z'.toByte()) {
+ i++
+ continue
+ }
+
+ // This string is needs to be lowercased. Create and return a new byte string.
+ val lowercase = data.copyOf()
+ lowercase[i++] = (c - ('A' - 'a')).toByte()
+ while (i < lowercase.size) {
+ c = lowercase[i]
+ if (c < 'A'.toByte() || c > 'Z'.toByte()) {
+ i++
+ continue
+ }
+ lowercase[i] = (c - ('A' - 'a')).toByte()
+ i++
+ }
+ return ByteString(lowercase)
+ }
+ return this
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonToAsciiUppercase(): ByteString {
+ // Search for an lowercase character. If we don't find one, return this.
+ var i = 0
+ while (i < data.size) {
+ var c = data[i]
+ if (c < 'a'.toByte() || c > 'z'.toByte()) {
+ i++
+ continue
+ }
+
+ // This string is needs to be uppercased. Create and return a new byte string.
+ val lowercase = data.copyOf()
+ lowercase[i++] = (c - ('a' - 'A')).toByte()
+ while (i < lowercase.size) {
+ c = lowercase[i]
+ if (c < 'a'.toByte() || c > 'z'.toByte()) {
+ i++
+ continue
+ }
+ lowercase[i] = (c - ('a' - 'A')).toByte()
+ i++
+ }
+ return ByteString(lowercase)
+ }
+ return this
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonSubstring(beginIndex: Int, endIndex: Int): ByteString {
+ require(beginIndex >= 0) { "beginIndex < 0" }
+ require(endIndex <= data.size) { "endIndex > length(${data.size})" }
+
+ val subLen = endIndex - beginIndex
+ require(subLen >= 0) { "endIndex < beginIndex" }
+
+ if (beginIndex == 0 && endIndex == data.size) {
+ return this
+ }
+ return ByteString(data.copyOfRange(beginIndex, endIndex))
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonGetByte(pos: Int) = data[pos]
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonGetSize() = data.size
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonToByteArray() = data.copyOf()
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonInternalArray() = data
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonRangeEquals(
+ offset: Int,
+ other: ByteString,
+ otherOffset: Int,
+ byteCount: Int
+): Boolean = other.rangeEquals(otherOffset, this.data, offset, byteCount)
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonRangeEquals(
+ offset: Int,
+ other: ByteArray,
+ otherOffset: Int,
+ byteCount: Int
+): Boolean {
+ return (
+ offset >= 0 && offset <= data.size - byteCount &&
+ otherOffset >= 0 && otherOffset <= other.size - byteCount &&
+ arrayRangeEquals(data, offset, other, otherOffset, byteCount)
+ )
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonStartsWith(prefix: ByteString) =
+ rangeEquals(0, prefix, 0, prefix.size)
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonStartsWith(prefix: ByteArray) =
+ rangeEquals(0, prefix, 0, prefix.size)
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonEndsWith(suffix: ByteString) =
+ rangeEquals(size - suffix.size, suffix, 0, suffix.size)
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonEndsWith(suffix: ByteArray) =
+ rangeEquals(size - suffix.size, suffix, 0, suffix.size)
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonIndexOf(other: ByteArray, fromIndex: Int): Int {
+ val limit = data.size - other.size
+ for (i in maxOf(fromIndex, 0)..limit) {
+ if (arrayRangeEquals(data, i, other, 0, other.size)) {
+ return i
+ }
+ }
+ return -1
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonLastIndexOf(
+ other: ByteString,
+ fromIndex: Int
+) = lastIndexOf(other.internalArray(), fromIndex)
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonLastIndexOf(other: ByteArray, fromIndex: Int): Int {
+ val limit = data.size - other.size
+ for (i in minOf(fromIndex, limit) downTo 0) {
+ if (arrayRangeEquals(data, i, other, 0, other.size)) {
+ return i
+ }
+ }
+ return -1
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonEquals(other: Any?): Boolean {
+ return when {
+ other === this -> true
+ other is ByteString -> other.size == data.size && other.rangeEquals(0, data, 0, data.size)
+ else -> false
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonHashCode(): Int {
+ val result = hashCode
+ if (result != 0) return result
+ return data.contentHashCode().also {
+ hashCode = it
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonCompareTo(other: ByteString): Int {
+ val sizeA = size
+ val sizeB = other.size
+ var i = 0
+ val size = minOf(sizeA, sizeB)
+ while (i < size) {
+ val byteA = this[i] and 0xff
+ val byteB = other[i] and 0xff
+ if (byteA == byteB) {
+ i++
+ continue
+ }
+ return if (byteA < byteB) -1 else 1
+ }
+ if (sizeA == sizeB) return 0
+ return if (sizeA < sizeB) -1 else 1
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun commonOf(data: ByteArray) = ByteString(data.copyOf())
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteArray.commonToByteString(offset: Int, byteCount: Int): ByteString {
+ checkOffsetAndCount(size.toLong(), offset.toLong(), byteCount.toLong())
+ return ByteString(copyOfRange(offset, offset + byteCount))
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun String.commonEncodeUtf8(): ByteString {
+ val byteString = ByteString(asUtf8ToByteArray())
+ byteString.utf8 = this
+ return byteString
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun String.commonDecodeBase64(): ByteString? {
+ val decoded = decodeBase64ToArray()
+ return if (decoded != null) ByteString(decoded) else null
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun String.commonDecodeHex(): ByteString {
+ require(length % 2 == 0) { "Unexpected hex string: $this" }
+
+ val result = ByteArray(length / 2)
+ for (i in result.indices) {
+ val d1 = decodeHexDigit(this[i * 2]) shl 4
+ val d2 = decodeHexDigit(this[i * 2 + 1])
+ result[i] = (d1 + d2).toByte()
+ }
+ return ByteString(result)
+}
+
+/** Writes the contents of this byte string to `buffer`. */
+internal fun ByteString.commonWrite(buffer: Buffer, offset: Int, byteCount: Int) {
+ buffer.write(data, offset, byteCount)
+}
+
+private fun decodeHexDigit(c: Char): Int {
+ return when (c) {
+ in '0'..'9' -> c - '0'
+ in 'a'..'f' -> c - 'a' + 10
+ in 'A'..'F' -> c - 'A' + 10
+ else -> throw IllegalArgumentException("Unexpected hex digit: $c")
+ }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun ByteString.commonToString(): String {
+ if (data.isEmpty()) return "[size=0]"
+
+ val i = codePointIndexToCharIndex(data, 64)
+ if (i == -1) {
+ return if (data.size <= 64) {
+ "[hex=${hex()}]"
+ } else {
+ "[size=${data.size} hex=${commonSubstring(0, 64).hex()}…]"
+ }
+ }
+
+ val text = utf8()
+ val safeText = text.substring(0, i)
+ .replace("\\", "\\\\")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ return if (i < text.length) {
+ "[size=${data.size} text=$safeText…]"
+ } else {
+ "[text=$safeText]"
+ }
+}
+
+private fun codePointIndexToCharIndex(s: ByteArray, codePointCount: Int): Int {
+ var charCount = 0
+ var j = 0
+ s.processUtf8CodePoints(0, s.size) { c ->
+ if (j++ == codePointCount) {
+ return charCount
+ }
+
+ if ((c != '\n'.toInt() && c != '\r'.toInt() && isIsoControl(c)) ||
+ c == REPLACEMENT_CODE_POINT
+ ) {
+ return -1
+ }
+
+ charCount += if (c < 0x10000) 1 else 2
+ }
+ return charCount
+}
diff --git a/okio/src/commonMain/kotlin/okio/internal/RealBufferedSink.kt b/okio/src/commonMain/kotlin/okio/internal/RealBufferedSink.kt
new file mode 100644
index 00000000..49b0c4d7
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/internal/RealBufferedSink.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO move to RealBufferedSink class: https://youtrack.jetbrains.com/issue/KT-20427
+@file:Suppress("NOTHING_TO_INLINE")
+
+package okio.internal
+
+import okio.Buffer
+import okio.BufferedSink
+import okio.ByteString
+import okio.EOFException
+import okio.RealBufferedSink
+import okio.Segment
+import okio.Source
+
+internal inline fun RealBufferedSink.commonWrite(source: Buffer, byteCount: Long) {
+ check(!closed) { "closed" }
+ buffer.write(source, byteCount)
+ emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWrite(byteString: ByteString): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.write(byteString)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWrite(
+ byteString: ByteString,
+ offset: Int,
+ byteCount: Int
+): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.write(byteString, offset, byteCount)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteUtf8(string: String): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeUtf8(string)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteUtf8(
+ string: String,
+ beginIndex: Int,
+ endIndex: Int
+): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeUtf8(string, beginIndex, endIndex)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteUtf8CodePoint(codePoint: Int): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeUtf8CodePoint(codePoint)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWrite(source: ByteArray): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.write(source)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWrite(
+ source: ByteArray,
+ offset: Int,
+ byteCount: Int
+): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.write(source, offset, byteCount)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteAll(source: Source): Long {
+ var totalBytesRead = 0L
+ while (true) {
+ val readCount: Long = source.read(buffer, Segment.SIZE.toLong())
+ if (readCount == -1L) break
+ totalBytesRead += readCount
+ emitCompleteSegments()
+ }
+ return totalBytesRead
+}
+
+internal inline fun RealBufferedSink.commonWrite(source: Source, byteCount: Long): BufferedSink {
+ var byteCount = byteCount
+ while (byteCount > 0L) {
+ val read = source.read(buffer, byteCount)
+ if (read == -1L) throw EOFException()
+ byteCount -= read
+ emitCompleteSegments()
+ }
+ return this
+}
+
+internal inline fun RealBufferedSink.commonWriteByte(b: Int): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeByte(b)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteShort(s: Int): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeShort(s)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteShortLe(s: Int): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeShortLe(s)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteInt(i: Int): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeInt(i)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteIntLe(i: Int): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeIntLe(i)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteLong(v: Long): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeLong(v)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteLongLe(v: Long): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeLongLe(v)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteDecimalLong(v: Long): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeDecimalLong(v)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonWriteHexadecimalUnsignedLong(v: Long): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeHexadecimalUnsignedLong(v)
+ return emitCompleteSegments()
+}
+
+internal inline fun RealBufferedSink.commonEmitCompleteSegments(): BufferedSink {
+ check(!closed) { "closed" }
+ val byteCount = buffer.completeSegmentByteCount()
+ if (byteCount > 0L) sink.write(buffer, byteCount)
+ return this
+}
+
+internal inline fun RealBufferedSink.commonEmit(): BufferedSink {
+ check(!closed) { "closed" }
+ val byteCount = buffer.size
+ if (byteCount > 0L) sink.write(buffer, byteCount)
+ return this
+}
+
+internal inline fun RealBufferedSink.commonFlush() {
+ check(!closed) { "closed" }
+ if (buffer.size > 0L) {
+ sink.write(buffer, buffer.size)
+ }
+ sink.flush()
+}
+
+internal inline fun RealBufferedSink.commonClose() {
+ if (closed) return
+
+ // Emit buffered data to the underlying sink. If this fails, we still need
+ // to close the sink; otherwise we risk leaking resources.
+ var thrown: Throwable? = null
+ try {
+ if (buffer.size > 0) {
+ sink.write(buffer, buffer.size)
+ }
+ } catch (e: Throwable) {
+ thrown = e
+ }
+
+ try {
+ sink.close()
+ } catch (e: Throwable) {
+ if (thrown == null) thrown = e
+ }
+
+ closed = true
+
+ if (thrown != null) throw thrown
+}
+
+internal inline fun RealBufferedSink.commonTimeout() = sink.timeout()
+
+internal inline fun RealBufferedSink.commonToString() = "buffer($sink)"
diff --git a/okio/src/commonMain/kotlin/okio/internal/RealBufferedSource.kt b/okio/src/commonMain/kotlin/okio/internal/RealBufferedSource.kt
new file mode 100644
index 00000000..4b901437
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/internal/RealBufferedSource.kt
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO move to RealBufferedSource class: https://youtrack.jetbrains.com/issue/KT-20427
+@file:Suppress("NOTHING_TO_INLINE")
+
+package okio.internal
+
+import okio.Buffer
+import okio.BufferedSource
+import okio.ByteString
+import okio.EOFException
+import okio.Options
+import okio.PeekSource
+import okio.RealBufferedSource
+import okio.Segment
+import okio.Sink
+import okio.buffer
+import okio.checkOffsetAndCount
+
+internal inline fun RealBufferedSource.commonRead(sink: Buffer, byteCount: Long): Long {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ check(!closed) { "closed" }
+
+ if (buffer.size == 0L) {
+ val read = source.read(buffer, Segment.SIZE.toLong())
+ if (read == -1L) return -1L
+ }
+
+ val toRead = minOf(byteCount, buffer.size)
+ return buffer.read(sink, toRead)
+}
+
+internal inline fun RealBufferedSource.commonExhausted(): Boolean {
+ check(!closed) { "closed" }
+ return buffer.exhausted() && source.read(buffer, Segment.SIZE.toLong()) == -1L
+}
+
+internal inline fun RealBufferedSource.commonRequire(byteCount: Long) {
+ if (!request(byteCount)) throw EOFException()
+}
+
+internal inline fun RealBufferedSource.commonRequest(byteCount: Long): Boolean {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ check(!closed) { "closed" }
+ while (buffer.size < byteCount) {
+ if (source.read(buffer, Segment.SIZE.toLong()) == -1L) return false
+ }
+ return true
+}
+
+internal inline fun RealBufferedSource.commonReadByte(): Byte {
+ require(1)
+ return buffer.readByte()
+}
+
+internal inline fun RealBufferedSource.commonReadByteString(): ByteString {
+ buffer.writeAll(source)
+ return buffer.readByteString()
+}
+
+internal inline fun RealBufferedSource.commonReadByteString(byteCount: Long): ByteString {
+ require(byteCount)
+ return buffer.readByteString(byteCount)
+}
+
+internal inline fun RealBufferedSource.commonSelect(options: Options): Int {
+ check(!closed) { "closed" }
+
+ while (true) {
+ val index = buffer.selectPrefix(options, selectTruncated = true)
+ when (index) {
+ -1 -> {
+ return -1
+ }
+ -2 -> {
+ // We need to grow the buffer. Do that, then try it all again.
+ if (source.read(buffer, Segment.SIZE.toLong()) == -1L) return -1
+ }
+ else -> {
+ // We matched a full byte string: consume it and return it.
+ val selectedSize = options.byteStrings[index].size
+ buffer.skip(selectedSize.toLong())
+ return index
+ }
+ }
+ }
+}
+
+internal inline fun RealBufferedSource.commonReadByteArray(): ByteArray {
+ buffer.writeAll(source)
+ return buffer.readByteArray()
+}
+
+internal inline fun RealBufferedSource.commonReadByteArray(byteCount: Long): ByteArray {
+ require(byteCount)
+ return buffer.readByteArray(byteCount)
+}
+
+internal inline fun RealBufferedSource.commonReadFully(sink: ByteArray) {
+ try {
+ require(sink.size.toLong())
+ } catch (e: EOFException) {
+ // The underlying source is exhausted. Copy the bytes we got before rethrowing.
+ var offset = 0
+ while (buffer.size > 0L) {
+ val read = buffer.read(sink, offset, buffer.size.toInt())
+ if (read == -1) throw AssertionError()
+ offset += read
+ }
+ throw e
+ }
+
+ buffer.readFully(sink)
+}
+
+internal inline fun RealBufferedSource.commonRead(sink: ByteArray, offset: Int, byteCount: Int): Int {
+ checkOffsetAndCount(sink.size.toLong(), offset.toLong(), byteCount.toLong())
+
+ if (buffer.size == 0L) {
+ val read = source.read(buffer, Segment.SIZE.toLong())
+ if (read == -1L) return -1
+ }
+
+ val toRead = okio.minOf(byteCount, buffer.size).toInt()
+ return buffer.read(sink, offset, toRead)
+}
+
+internal inline fun RealBufferedSource.commonReadFully(sink: Buffer, byteCount: Long) {
+ try {
+ require(byteCount)
+ } catch (e: EOFException) {
+ // The underlying source is exhausted. Copy the bytes we got before rethrowing.
+ sink.writeAll(buffer)
+ throw e
+ }
+
+ buffer.readFully(sink, byteCount)
+}
+
+internal inline fun RealBufferedSource.commonReadAll(sink: Sink): Long {
+ var totalBytesWritten: Long = 0
+ while (source.read(buffer, Segment.SIZE.toLong()) != -1L) {
+ val emitByteCount = buffer.completeSegmentByteCount()
+ if (emitByteCount > 0L) {
+ totalBytesWritten += emitByteCount
+ sink.write(buffer, emitByteCount)
+ }
+ }
+ if (buffer.size > 0L) {
+ totalBytesWritten += buffer.size
+ sink.write(buffer, buffer.size)
+ }
+ return totalBytesWritten
+}
+
+internal inline fun RealBufferedSource.commonReadUtf8(): String {
+ buffer.writeAll(source)
+ return buffer.readUtf8()
+}
+
+internal inline fun RealBufferedSource.commonReadUtf8(byteCount: Long): String {
+ require(byteCount)
+ return buffer.readUtf8(byteCount)
+}
+
+internal inline fun RealBufferedSource.commonReadUtf8Line(): String? {
+ val newline = indexOf('\n'.toByte())
+
+ return if (newline == -1L) {
+ if (buffer.size != 0L) {
+ readUtf8(buffer.size)
+ } else {
+ null
+ }
+ } else {
+ buffer.readUtf8Line(newline)
+ }
+}
+
+internal inline fun RealBufferedSource.commonReadUtf8LineStrict(limit: Long): String {
+ require(limit >= 0) { "limit < 0: $limit" }
+ val scanLength = if (limit == Long.MAX_VALUE) Long.MAX_VALUE else limit + 1
+ val newline = indexOf('\n'.toByte(), 0, scanLength)
+ if (newline != -1L) return buffer.readUtf8Line(newline)
+ if (scanLength < Long.MAX_VALUE &&
+ request(scanLength) && buffer[scanLength - 1] == '\r'.toByte() &&
+ request(scanLength + 1) && buffer[scanLength] == '\n'.toByte()
+ ) {
+ return buffer.readUtf8Line(scanLength) // The line was 'limit' UTF-8 bytes followed by \r\n.
+ }
+ val data = Buffer()
+ buffer.copyTo(data, 0, okio.minOf(32, buffer.size))
+ throw EOFException(
+ "\\n not found: limit=" + minOf(buffer.size, limit) +
+ " content=" + data.readByteString().hex() + '…'.toString()
+ )
+}
+
+internal inline fun RealBufferedSource.commonReadUtf8CodePoint(): Int {
+ require(1)
+
+ val b0 = buffer[0].toInt()
+ when {
+ b0 and 0xe0 == 0xc0 -> require(2)
+ b0 and 0xf0 == 0xe0 -> require(3)
+ b0 and 0xf8 == 0xf0 -> require(4)
+ }
+
+ return buffer.readUtf8CodePoint()
+}
+
+internal inline fun RealBufferedSource.commonReadShort(): Short {
+ require(2)
+ return buffer.readShort()
+}
+
+internal inline fun RealBufferedSource.commonReadShortLe(): Short {
+ require(2)
+ return buffer.readShortLe()
+}
+
+internal inline fun RealBufferedSource.commonReadInt(): Int {
+ require(4)
+ return buffer.readInt()
+}
+
+internal inline fun RealBufferedSource.commonReadIntLe(): Int {
+ require(4)
+ return buffer.readIntLe()
+}
+
+internal inline fun RealBufferedSource.commonReadLong(): Long {
+ require(8)
+ return buffer.readLong()
+}
+
+internal inline fun RealBufferedSource.commonReadLongLe(): Long {
+ require(8)
+ return buffer.readLongLe()
+}
+
+internal inline fun RealBufferedSource.commonReadDecimalLong(): Long {
+ require(1)
+
+ var pos = 0L
+ while (request(pos + 1)) {
+ val b = buffer[pos]
+ if ((b < '0'.toByte() || b > '9'.toByte()) && (pos != 0L || b != '-'.toByte())) {
+ // Non-digit, or non-leading negative sign.
+ if (pos == 0L) {
+ throw NumberFormatException("Expected leading [0-9] or '-' character but was 0x${b.toString(16)}")
+ }
+ break
+ }
+ pos++
+ }
+
+ return buffer.readDecimalLong()
+}
+
+internal inline fun RealBufferedSource.commonReadHexadecimalUnsignedLong(): Long {
+ require(1)
+
+ var pos = 0
+ while (request((pos + 1).toLong())) {
+ val b = buffer[pos.toLong()]
+ if ((b < '0'.toByte() || b > '9'.toByte()) &&
+ (b < 'a'.toByte() || b > 'f'.toByte()) &&
+ (b < 'A'.toByte() || b > 'F'.toByte())
+ ) {
+ // Non-digit, or non-leading negative sign.
+ if (pos == 0) {
+ throw NumberFormatException("Expected leading [0-9a-fA-F] character but was 0x${b.toString(16)}")
+ }
+ break
+ }
+ pos++
+ }
+
+ return buffer.readHexadecimalUnsignedLong()
+}
+
+internal inline fun RealBufferedSource.commonSkip(byteCount: Long) {
+ var byteCount = byteCount
+ check(!closed) { "closed" }
+ while (byteCount > 0) {
+ if (buffer.size == 0L && source.read(buffer, Segment.SIZE.toLong()) == -1L) {
+ throw EOFException()
+ }
+ val toSkip = minOf(byteCount, buffer.size)
+ buffer.skip(toSkip)
+ byteCount -= toSkip
+ }
+}
+
+internal inline fun RealBufferedSource.commonIndexOf(b: Byte, fromIndex: Long, toIndex: Long): Long {
+ var fromIndex = fromIndex
+ check(!closed) { "closed" }
+ require(fromIndex in 0L..toIndex) { "fromIndex=$fromIndex toIndex=$toIndex" }
+
+ while (fromIndex < toIndex) {
+ val result = buffer.indexOf(b, fromIndex, toIndex)
+ if (result != -1L) return result
+
+ // The byte wasn't in the buffer. Give up if we've already reached our target size or if the
+ // underlying stream is exhausted.
+ val lastBufferSize = buffer.size
+ if (lastBufferSize >= toIndex || source.read(buffer, Segment.SIZE.toLong()) == -1L) return -1L
+
+ // Continue the search from where we left off.
+ fromIndex = maxOf(fromIndex, lastBufferSize)
+ }
+ return -1L
+}
+
+internal inline fun RealBufferedSource.commonIndexOf(bytes: ByteString, fromIndex: Long): Long {
+ var fromIndex = fromIndex
+ check(!closed) { "closed" }
+
+ while (true) {
+ val result = buffer.indexOf(bytes, fromIndex)
+ if (result != -1L) return result
+
+ val lastBufferSize = buffer.size
+ if (source.read(buffer, Segment.SIZE.toLong()) == -1L) return -1L
+
+ // Keep searching, picking up from where we left off.
+ fromIndex = maxOf(fromIndex, lastBufferSize - bytes.size + 1)
+ }
+}
+
+internal inline fun RealBufferedSource.commonIndexOfElement(targetBytes: ByteString, fromIndex: Long): Long {
+ var fromIndex = fromIndex
+ check(!closed) { "closed" }
+
+ while (true) {
+ val result = buffer.indexOfElement(targetBytes, fromIndex)
+ if (result != -1L) return result
+
+ val lastBufferSize = buffer.size
+ if (source.read(buffer, Segment.SIZE.toLong()) == -1L) return -1L
+
+ // Keep searching, picking up from where we left off.
+ fromIndex = maxOf(fromIndex, lastBufferSize)
+ }
+}
+
+internal inline fun RealBufferedSource.commonRangeEquals(
+ offset: Long,
+ bytes: ByteString,
+ bytesOffset: Int,
+ byteCount: Int
+): Boolean {
+ check(!closed) { "closed" }
+
+ if (offset < 0L ||
+ bytesOffset < 0 ||
+ byteCount < 0 ||
+ bytes.size - bytesOffset < byteCount
+ ) {
+ return false
+ }
+ for (i in 0 until byteCount) {
+ val bufferOffset = offset + i
+ if (!request(bufferOffset + 1)) return false
+ if (buffer[bufferOffset] != bytes[bytesOffset + i]) return false
+ }
+ return true
+}
+
+internal inline fun RealBufferedSource.commonPeek(): BufferedSource {
+ return PeekSource(this).buffer()
+}
+
+internal inline fun RealBufferedSource.commonClose() {
+ if (closed) return
+ closed = true
+ source.close()
+ buffer.clear()
+}
+
+internal inline fun RealBufferedSource.commonTimeout() = source.timeout()
+
+internal inline fun RealBufferedSource.commonToString() = "buffer($source)"
diff --git a/okio/src/commonMain/kotlin/okio/internal/SegmentedByteString.kt b/okio/src/commonMain/kotlin/okio/internal/SegmentedByteString.kt
new file mode 100644
index 00000000..f46e1389
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/internal/SegmentedByteString.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// TODO move to SegmentedByteString class: https://youtrack.jetbrains.com/issue/KT-20427
+@file:Suppress("NOTHING_TO_INLINE")
+
+package okio.internal
+
+import okio.Buffer
+import okio.ByteString
+import okio.Segment
+import okio.SegmentedByteString
+import okio.arrayRangeEquals
+import okio.checkOffsetAndCount
+
+internal fun IntArray.binarySearch(value: Int, fromIndex: Int, toIndex: Int): Int {
+ var left = fromIndex
+ var right = toIndex - 1
+
+ while (left <= right) {
+ val mid = (left + right) ushr 1 // protect from overflow
+ val midVal = this[mid]
+
+ when {
+ midVal < value -> left = mid + 1
+ midVal > value -> right = mid - 1
+ else -> return mid
+ }
+ }
+
+ // no exact match, return negative of where it should match
+ return -left - 1
+}
+
+/** Returns the index of the segment that contains the byte at `pos`. */
+internal fun SegmentedByteString.segment(pos: Int): Int {
+ // Search for (pos + 1) instead of (pos) because the directory holds sizes, not indexes.
+ val i = directory.binarySearch(pos + 1, 0, segments.size)
+ return if (i >= 0) i else i.inv() // If i is negative, bitflip to get the insert position.
+}
+
+/** Processes all segments, invoking `action` with the ByteArray and range of valid data. */
+internal inline fun SegmentedByteString.forEachSegment(
+ action: (data: ByteArray, offset: Int, byteCount: Int) -> Unit
+) {
+ val segmentCount = segments.size
+ var s = 0
+ var pos = 0
+ while (s < segmentCount) {
+ val segmentPos = directory[segmentCount + s]
+ val nextSegmentOffset = directory[s]
+
+ action(segments[s], segmentPos, nextSegmentOffset - pos)
+ pos = nextSegmentOffset
+ s++
+ }
+}
+
+/**
+ * Processes the segments between `beginIndex` and `endIndex`, invoking `action` with the ByteArray
+ * and range of the valid data.
+ */
+private inline fun SegmentedByteString.forEachSegment(
+ beginIndex: Int,
+ endIndex: Int,
+ action: (data: ByteArray, offset: Int, byteCount: Int) -> Unit
+) {
+ var s = segment(beginIndex)
+ var pos = beginIndex
+ while (pos < endIndex) {
+ val segmentOffset = if (s == 0) 0 else directory[s - 1]
+ val segmentSize = directory[s] - segmentOffset
+ val segmentPos = directory[segments.size + s]
+
+ val byteCount = minOf(endIndex, segmentOffset + segmentSize) - pos
+ val offset = segmentPos + (pos - segmentOffset)
+ action(segments[s], offset, byteCount)
+ pos += byteCount
+ s++
+ }
+}
+
+// TODO Kotlin's expect classes can't have default implementations, so platform implementations
+// have to call these functions. Remove all this nonsense when expect class allow actual code.
+
+internal inline fun SegmentedByteString.commonSubstring(beginIndex: Int, endIndex: Int): ByteString {
+ require(beginIndex >= 0) { "beginIndex=$beginIndex < 0" }
+ require(endIndex <= size) { "endIndex=$endIndex > length($size)" }
+
+ val subLen = endIndex - beginIndex
+ require(subLen >= 0) { "endIndex=$endIndex < beginIndex=$beginIndex" }
+
+ when {
+ beginIndex == 0 && endIndex == size -> return this
+ beginIndex == endIndex -> return ByteString.EMPTY
+ }
+
+ val beginSegment = segment(beginIndex) // First segment to include
+ val endSegment = segment(endIndex - 1) // Last segment to include
+
+ val newSegments = segments.copyOfRange(beginSegment, endSegment + 1)
+ val newDirectory = IntArray(newSegments.size * 2)
+ var index = 0
+ for (s in beginSegment..endSegment) {
+ newDirectory[index] = minOf(directory[s] - beginIndex, subLen)
+ newDirectory[index++ + newSegments.size] = directory[s + segments.size]
+ }
+
+ // Set the new position of the first segment
+ val segmentOffset = if (beginSegment == 0) 0 else directory[beginSegment - 1]
+ newDirectory[newSegments.size] += beginIndex - segmentOffset
+
+ return SegmentedByteString(newSegments, newDirectory)
+}
+
+internal inline fun SegmentedByteString.commonInternalGet(pos: Int): Byte {
+ checkOffsetAndCount(directory[segments.size - 1].toLong(), pos.toLong(), 1)
+ val segment = segment(pos)
+ val segmentOffset = if (segment == 0) 0 else directory[segment - 1]
+ val segmentPos = directory[segment + segments.size]
+ return segments[segment][pos - segmentOffset + segmentPos]
+}
+
+internal inline fun SegmentedByteString.commonGetSize() = directory[segments.size - 1]
+
+internal inline fun SegmentedByteString.commonToByteArray(): ByteArray {
+ val result = ByteArray(size)
+ var resultPos = 0
+ forEachSegment { data, offset, byteCount ->
+ data.copyInto(
+ result, destinationOffset = resultPos, startIndex = offset,
+ endIndex = offset + byteCount
+ )
+ resultPos += byteCount
+ }
+ return result
+}
+
+internal inline fun SegmentedByteString.commonWrite(buffer: Buffer, offset: Int, byteCount: Int) {
+ forEachSegment(offset, offset + byteCount) { data, offset, byteCount ->
+ val segment = Segment(data, offset, offset + byteCount, true, false)
+ if (buffer.head == null) {
+ segment.prev = segment
+ segment.next = segment.prev
+ buffer.head = segment.next
+ } else {
+ buffer.head!!.prev!!.push(segment)
+ }
+ }
+ buffer.size += byteCount
+}
+
+internal inline fun SegmentedByteString.commonRangeEquals(
+ offset: Int,
+ other: ByteString,
+ otherOffset: Int,
+ byteCount: Int
+): Boolean {
+ if (offset < 0 || offset > size - byteCount) return false
+ // Go segment-by-segment through this, passing arrays to other's rangeEquals().
+ var otherOffset = otherOffset
+ forEachSegment(offset, offset + byteCount) { data, offset, byteCount ->
+ if (!other.rangeEquals(otherOffset, data, offset, byteCount)) return false
+ otherOffset += byteCount
+ }
+ return true
+}
+
+internal inline fun SegmentedByteString.commonRangeEquals(
+ offset: Int,
+ other: ByteArray,
+ otherOffset: Int,
+ byteCount: Int
+): Boolean {
+ if (offset < 0 || offset > size - byteCount ||
+ otherOffset < 0 || otherOffset > other.size - byteCount
+ ) {
+ return false
+ }
+ // Go segment-by-segment through this, comparing ranges of arrays.
+ var otherOffset = otherOffset
+ forEachSegment(offset, offset + byteCount) { data, offset, byteCount ->
+ if (!arrayRangeEquals(data, offset, other, otherOffset, byteCount)) return false
+ otherOffset += byteCount
+ }
+ return true
+}
+
+internal inline fun SegmentedByteString.commonEquals(other: Any?): Boolean {
+ return when {
+ other === this -> true
+ other is ByteString -> other.size == size && rangeEquals(0, other, 0, size)
+ else -> false
+ }
+}
+
+internal inline fun SegmentedByteString.commonHashCode(): Int {
+ var result = hashCode
+ if (result != 0) return result
+
+ // Equivalent to Arrays.hashCode(toByteArray()).
+ result = 1
+ forEachSegment { data, offset, byteCount ->
+ var i = offset
+ val limit = offset + byteCount
+ while (i < limit) {
+ result = 31 * result + data[i]
+ i++
+ }
+ }
+ hashCode = result
+ return result
+}
diff --git a/okio/src/commonTest/kotlin/okio/AbstractBufferedSinkTest.kt b/okio/src/commonTest/kotlin/okio/AbstractBufferedSinkTest.kt
new file mode 100644
index 00000000..49bff8d9
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/AbstractBufferedSinkTest.kt
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.ByteString.Companion.decodeHex
+import okio.ByteString.Companion.encodeUtf8
+import kotlin.math.pow
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class BufferSinkTest : AbstractBufferedSinkTest(BufferedSinkFactory.BUFFER)
+class RealBufferedSinkTest : AbstractBufferedSinkTest(BufferedSinkFactory.REAL_BUFFERED_SINK)
+
+abstract class AbstractBufferedSinkTest internal constructor(
+ factory: BufferedSinkFactory
+) {
+ private val data: Buffer = Buffer()
+ private val sink: BufferedSink = factory.create(data)
+
+ @Test fun writeNothing() {
+ sink.writeUtf8("")
+ sink.flush()
+ assertEquals(0, data.size)
+ }
+
+ @Test fun writeBytes() {
+ sink.writeByte(0xab)
+ sink.writeByte(0xcd)
+ sink.flush()
+ assertEquals("[hex=abcd]", data.toString())
+ }
+
+ @Test fun writeLastByteInSegment() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 1))
+ sink.writeByte(0x20)
+ sink.writeByte(0x21)
+ sink.flush()
+ assertEquals(listOf(Segment.SIZE, 1), segmentSizes(data))
+ assertEquals("a".repeat(Segment.SIZE - 1), data.readUtf8(Segment.SIZE - 1L))
+ assertEquals("[text= !]", data.toString())
+ }
+
+ @Test fun writeShort() {
+ sink.writeShort(0xabcd)
+ sink.writeShort(0x4321)
+ sink.flush()
+ assertEquals("[hex=abcd4321]", data.toString())
+ }
+
+ @Test fun writeShortLe() {
+ sink.writeShortLe(0xcdab)
+ sink.writeShortLe(0x2143)
+ sink.flush()
+ assertEquals("[hex=abcd4321]", data.toString())
+ }
+
+ @Test fun writeInt() {
+ sink.writeInt(-0x543210ff)
+ sink.writeInt(-0x789abcdf)
+ sink.flush()
+ assertEquals("[hex=abcdef0187654321]", data.toString())
+ }
+
+ @Test fun writeLastIntegerInSegment() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 4))
+ sink.writeInt(-0x543210ff)
+ sink.writeInt(-0x789abcdf)
+ sink.flush()
+ assertEquals(listOf(Segment.SIZE, 4), segmentSizes(data))
+ assertEquals("a".repeat(Segment.SIZE - 4), data.readUtf8(Segment.SIZE - 4L))
+ assertEquals("[hex=abcdef0187654321]", data.toString())
+ }
+
+ @Test fun writeIntegerDoesNotQuiteFitInSegment() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 3))
+ sink.writeInt(-0x543210ff)
+ sink.writeInt(-0x789abcdf)
+ sink.flush()
+ assertEquals(listOf(Segment.SIZE - 3, 8), segmentSizes(data))
+ assertEquals("a".repeat(Segment.SIZE - 3), data.readUtf8(Segment.SIZE - 3L))
+ assertEquals("[hex=abcdef0187654321]", data.toString())
+ }
+
+ @Test fun writeIntLe() {
+ sink.writeIntLe(-0x543210ff)
+ sink.writeIntLe(-0x789abcdf)
+ sink.flush()
+ assertEquals("[hex=01efcdab21436587]", data.toString())
+ }
+
+ @Test fun writeLong() {
+ sink.writeLong(-0x543210fe789abcdfL)
+ sink.writeLong(-0x350145414f4ea400L)
+ sink.flush()
+ assertEquals("[hex=abcdef0187654321cafebabeb0b15c00]", data.toString())
+ }
+
+ @Test fun writeLongLe() {
+ sink.writeLongLe(-0x543210fe789abcdfL)
+ sink.writeLongLe(-0x350145414f4ea400L)
+ sink.flush()
+ assertEquals("[hex=2143658701efcdab005cb1b0bebafeca]", data.toString())
+ }
+
+ @Test fun writeByteString() {
+ sink.write("təˈranəˌsôr".encodeUtf8())
+ sink.flush()
+ assertEquals("74c999cb8872616ec999cb8c73c3b472".decodeHex(), data.readByteString())
+ }
+
+ @Test fun writeByteStringOffset() {
+ sink.write("təˈranəˌsôr".encodeUtf8(), 5, 5)
+ sink.flush()
+ assertEquals("72616ec999".decodeHex(), data.readByteString())
+ }
+
+ @Test fun writeSegmentedByteString() {
+ sink.write(Buffer().write("təˈranəˌsôr".encodeUtf8()).snapshot())
+ sink.flush()
+ assertEquals("74c999cb8872616ec999cb8c73c3b472".decodeHex(), data.readByteString())
+ }
+
+ @Test fun writeSegmentedByteStringOffset() {
+ sink.write(Buffer().write("təˈranəˌsôr".encodeUtf8()).snapshot(), 5, 5)
+ sink.flush()
+ assertEquals("72616ec999".decodeHex(), data.readByteString())
+ }
+
+ @Test fun writeStringUtf8() {
+ sink.writeUtf8("təˈranəˌsôr")
+ sink.flush()
+ assertEquals("74c999cb8872616ec999cb8c73c3b472".decodeHex(), data.readByteString())
+ }
+
+ @Test fun writeSubstringUtf8() {
+ sink.writeUtf8("təˈranəˌsôr", 3, 7)
+ sink.flush()
+ assertEquals("72616ec999".decodeHex(), data.readByteString())
+ }
+
+ @Test fun writeAll() {
+ val source = Buffer().writeUtf8("abcdef")
+
+ assertEquals(6, sink.writeAll(source))
+ assertEquals(0, source.size)
+ sink.flush()
+ assertEquals("abcdef", data.readUtf8())
+ }
+
+ @Test fun writeSource() {
+ val source = Buffer().writeUtf8("abcdef")
+
+ // Force resolution of the Source method overload.
+ sink.write(source as Source, 4)
+ sink.flush()
+ assertEquals("abcd", data.readUtf8())
+ assertEquals("ef", source.readUtf8())
+ }
+
+ @Test fun writeSourceReadsFully() {
+ val source = object : Source by Buffer() {
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ sink.writeUtf8("abcd")
+ return 4
+ }
+ }
+
+ sink.write(source, 8)
+ sink.flush()
+ assertEquals("abcdabcd", data.readUtf8())
+ }
+
+ @Test fun writeSourcePropagatesEof() {
+ val source: Source = Buffer().writeUtf8("abcd")
+
+ assertFailsWith<EOFException> {
+ sink.write(source, 8)
+ }
+
+ // Ensure that whatever was available was correctly written.
+ sink.flush()
+ assertEquals("abcd", data.readUtf8())
+ }
+
+ @Test fun writeSourceWithZeroIsNoOp() {
+ // This test ensures that a zero byte count never calls through to read the source. It may be
+ // tied to something like a socket which will potentially block trying to read a segment when
+ // ultimately we don't want any data.
+ val source = object : Source by Buffer() {
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ throw AssertionError()
+ }
+ }
+ sink.write(source, 0)
+ assertEquals(0, data.size)
+ }
+
+ @Test fun writeAllExhausted() {
+ val source = Buffer()
+ assertEquals(0, sink.writeAll(source))
+ assertEquals(0, source.size)
+ }
+
+ @Test fun closeEmitsBufferedBytes() {
+ sink.writeByte('a'.toInt())
+ sink.close()
+ assertEquals('a', data.readByte().toChar())
+ }
+
+ @Test fun longDecimalString() {
+ assertLongDecimalString(0)
+ assertLongDecimalString(Long.MIN_VALUE)
+ assertLongDecimalString(Long.MAX_VALUE)
+
+ for (i in 1..19) {
+ val value = 10.0.pow(i).toLong()
+ assertLongDecimalString(value - 1)
+ assertLongDecimalString(value)
+ }
+ }
+
+ private fun assertLongDecimalString(value: Long) {
+ sink.writeDecimalLong(value).writeUtf8("zzz").flush()
+ val expected = "${value}zzz"
+ val actual = data.readUtf8()
+ assertEquals(expected, actual, "$value expected $expected but was $actual")
+ }
+
+ @Test fun longHexString() {
+ assertLongHexString(0)
+ assertLongHexString(Long.MIN_VALUE)
+ assertLongHexString(Long.MAX_VALUE)
+
+ for (i in 0..62) {
+ assertLongHexString((1L shl i) - 1)
+ assertLongHexString(1L shl i)
+ }
+ }
+
+ private fun assertLongHexString(value: Long) {
+ sink.writeHexadecimalUnsignedLong(value).writeUtf8("zzz").flush()
+ val expected = "${value.toHexString()}zzz"
+ val actual = data.readUtf8()
+ assertEquals(expected, actual, "$value expected $expected but was $actual")
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/AbstractBufferedSourceTest.kt b/okio/src/commonTest/kotlin/okio/AbstractBufferedSourceTest.kt
new file mode 100644
index 00000000..b15d369c
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/AbstractBufferedSourceTest.kt
@@ -0,0 +1,1281 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.ByteString.Companion.decodeHex
+import okio.ByteString.Companion.encodeUtf8
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+class BufferSourceTest : AbstractBufferedSourceTest(BufferedSourceFactory.BUFFER)
+class RealBufferedSourceTest : AbstractBufferedSourceTest(BufferedSourceFactory.REAL_BUFFERED_SOURCE)
+class OneByteAtATimeBufferedSourceTest : AbstractBufferedSourceTest(BufferedSourceFactory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE)
+class OneByteAtATimeBufferTest : AbstractBufferedSourceTest(BufferedSourceFactory.ONE_BYTE_AT_A_TIME_BUFFER)
+class PeekBufferTest : AbstractBufferedSourceTest(BufferedSourceFactory.PEEK_BUFFER)
+class PeekBufferedSourceTest : AbstractBufferedSourceTest(BufferedSourceFactory.PEEK_BUFFERED_SOURCE)
+
+abstract class AbstractBufferedSourceTest internal constructor(
+ private val factory: BufferedSourceFactory
+) {
+ private val sink: BufferedSink
+ private val source: BufferedSource
+
+ init {
+ val pipe = factory.pipe()
+ sink = pipe.sink
+ source = pipe.source
+ }
+
+ @Test fun readBytes() {
+ sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte()))
+ sink.emit()
+ assertEquals(0xab, (source.readByte() and 0xff).toLong())
+ assertEquals(0xcd, (source.readByte() and 0xff).toLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readByteTooShortThrows() {
+ assertFailsWith<EOFException> {
+ source.readByte()
+ }
+ }
+
+ @Test fun readShort() {
+ sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x01.toByte()))
+ sink.emit()
+ assertEquals(0xabcd.toShort().toLong(), source.readShort().toLong())
+ assertEquals(0xef01.toShort().toLong(), source.readShort().toLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readShortLe() {
+ sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x10.toByte()))
+ sink.emit()
+ assertEquals(0xcdab.toShort().toLong(), source.readShortLe().toLong())
+ assertEquals(0x10ef.toShort().toLong(), source.readShortLe().toLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readShortSplitAcrossMultipleSegments() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 1))
+ sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte()))
+ sink.emit()
+ source.skip((Segment.SIZE - 1).toLong())
+ assertEquals(0xabcd.toShort().toLong(), source.readShort().toLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readShortTooShortThrows() {
+ sink.writeShort(Short.MAX_VALUE.toInt())
+ sink.emit()
+ source.readByte()
+ assertFailsWith<EOFException> {
+ source.readShort()
+ }
+ }
+
+ @Test fun readShortLeTooShortThrows() {
+ sink.writeShortLe(Short.MAX_VALUE.toInt())
+ sink.emit()
+ source.readByte()
+ assertFailsWith<EOFException> {
+ source.readShortLe()
+ }
+ }
+
+ @Test fun readInt() {
+ sink.write(
+ byteArrayOf(
+ 0xab.toByte(),
+ 0xcd.toByte(),
+ 0xef.toByte(),
+ 0x01.toByte(),
+ 0x87.toByte(),
+ 0x65.toByte(),
+ 0x43.toByte(),
+ 0x21.toByte()
+ )
+ )
+ sink.emit()
+ assertEquals(-0x543210ff, source.readInt().toLong())
+ assertEquals(-0x789abcdf, source.readInt().toLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readIntLe() {
+ sink.write(
+ byteArrayOf(
+ 0xab.toByte(),
+ 0xcd.toByte(),
+ 0xef.toByte(),
+ 0x10.toByte(),
+ 0x87.toByte(),
+ 0x65.toByte(),
+ 0x43.toByte(),
+ 0x21.toByte()
+ )
+ )
+ sink.emit()
+ assertEquals(0x10efcdab, source.readIntLe().toLong())
+ assertEquals(0x21436587, source.readIntLe().toLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readIntSplitAcrossMultipleSegments() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 3))
+ sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x01.toByte()))
+ sink.emit()
+ source.skip((Segment.SIZE - 3).toLong())
+ assertEquals(-0x543210ff, source.readInt().toLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readIntTooShortThrows() {
+ sink.writeInt(Int.MAX_VALUE)
+ sink.emit()
+ source.readByte()
+ assertFailsWith<EOFException> {
+ source.readInt()
+ }
+ }
+
+ @Test fun readIntLeTooShortThrows() {
+ sink.writeIntLe(Int.MAX_VALUE)
+ sink.emit()
+ source.readByte()
+ assertFailsWith<EOFException> {
+ source.readIntLe()
+ }
+ }
+
+ @Test fun readLong() {
+ sink.write(
+ byteArrayOf(
+ 0xab.toByte(),
+ 0xcd.toByte(),
+ 0xef.toByte(),
+ 0x10.toByte(),
+ 0x87.toByte(),
+ 0x65.toByte(),
+ 0x43.toByte(),
+ 0x21.toByte(),
+ 0x36.toByte(),
+ 0x47.toByte(),
+ 0x58.toByte(),
+ 0x69.toByte(),
+ 0x12.toByte(),
+ 0x23.toByte(),
+ 0x34.toByte(),
+ 0x45.toByte()
+ )
+ )
+ sink.emit()
+ assertEquals(-0x543210ef789abcdfL, source.readLong())
+ assertEquals(0x3647586912233445L, source.readLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readLongLe() {
+ sink.write(
+ byteArrayOf(
+ 0xab.toByte(),
+ 0xcd.toByte(),
+ 0xef.toByte(),
+ 0x10.toByte(),
+ 0x87.toByte(),
+ 0x65.toByte(),
+ 0x43.toByte(),
+ 0x21.toByte(),
+ 0x36.toByte(),
+ 0x47.toByte(),
+ 0x58.toByte(),
+ 0x69.toByte(),
+ 0x12.toByte(),
+ 0x23.toByte(),
+ 0x34.toByte(),
+ 0x45.toByte()
+ )
+ )
+ sink.emit()
+ assertEquals(0x2143658710efcdabL, source.readLongLe())
+ assertEquals(0x4534231269584736L, source.readLongLe())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readLongSplitAcrossMultipleSegments() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 7))
+ sink.write(
+ byteArrayOf(
+ 0xab.toByte(),
+ 0xcd.toByte(),
+ 0xef.toByte(),
+ 0x01.toByte(),
+ 0x87.toByte(),
+ 0x65.toByte(),
+ 0x43.toByte(),
+ 0x21.toByte()
+ )
+ )
+ sink.emit()
+ source.skip((Segment.SIZE - 7).toLong())
+ assertEquals(-0x543210fe789abcdfL, source.readLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readLongTooShortThrows() {
+ sink.writeLong(Long.MAX_VALUE)
+ sink.emit()
+ source.readByte()
+ assertFailsWith<EOFException> {
+ source.readLong()
+ }
+ }
+
+ @Test fun readLongLeTooShortThrows() {
+ sink.writeLongLe(Long.MAX_VALUE)
+ sink.emit()
+ source.readByte()
+ assertFailsWith<EOFException> {
+ source.readLongLe()
+ }
+ }
+
+ @Test fun readAll() {
+ source.buffer.writeUtf8("abc")
+ sink.writeUtf8("def")
+ sink.emit()
+
+ val sink = Buffer()
+ assertEquals(6, source.readAll(sink))
+ assertEquals("abcdef", sink.readUtf8())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readAllExhausted() {
+ val mockSink = MockSink()
+ assertEquals(0, source.readAll(mockSink))
+ assertTrue(source.exhausted())
+ mockSink.assertLog()
+ }
+
+ @Test fun readExhaustedSource() {
+ val sink = Buffer()
+ sink.writeUtf8("a".repeat(10))
+ assertEquals(-1, source.read(sink, 10))
+ assertEquals(10, sink.size)
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readZeroBytesFromSource() {
+ val sink = Buffer()
+ sink.writeUtf8("a".repeat(10))
+
+ // Either 0 or -1 is reasonable here. For consistency with Android's
+ // ByteArrayInputStream we return 0.
+ assertEquals(-1, source.read(sink, 0))
+ assertEquals(10, sink.size)
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun readFully() {
+ sink.writeUtf8("a".repeat(10000))
+ sink.emit()
+ val sink = Buffer()
+ source.readFully(sink, 9999)
+ assertEquals("a".repeat(9999), sink.readUtf8())
+ assertEquals("a", source.readUtf8())
+ }
+
+ @Test fun readFullyTooShortThrows() {
+ sink.writeUtf8("Hi")
+ sink.emit()
+ val sink = Buffer()
+ assertFailsWith<EOFException> {
+ source.readFully(sink, 5)
+ }
+
+ // Verify we read all that we could from the source.
+ assertEquals("Hi", sink.readUtf8())
+ }
+
+ @Test fun readFullyByteArray() {
+ val data = Buffer()
+ data.writeUtf8("Hello").writeUtf8("e".repeat(Segment.SIZE))
+
+ val expected = data.copy().readByteArray()
+ sink.write(data, data.size)
+ sink.emit()
+
+ val sink = ByteArray(Segment.SIZE + 5)
+ source.readFully(sink)
+ assertArrayEquals(expected, sink)
+ }
+
+ @Test fun readFullyByteArrayTooShortThrows() {
+ sink.writeUtf8("Hello")
+ sink.emit()
+
+ val array = ByteArray(6)
+ assertFailsWith<EOFException> {
+ source.readFully(array)
+ }
+
+ // Verify we read all that we could from the source.
+ assertArrayEquals(
+ byteArrayOf(
+ 'H'.toByte(),
+ 'e'.toByte(),
+ 'l'.toByte(),
+ 'l'.toByte(),
+ 'o'.toByte(),
+ 0
+ ),
+ array
+ )
+ }
+
+ @Test fun readIntoByteArray() {
+ sink.writeUtf8("abcd")
+ sink.emit()
+
+ val sink = ByteArray(3)
+ val read = source.read(sink)
+ if (factory.isOneByteAtATime) {
+ assertEquals(1, read.toLong())
+ val expected = byteArrayOf('a'.toByte(), 0, 0)
+ assertArrayEquals(expected, sink)
+ } else {
+ assertEquals(3, read.toLong())
+ val expected = byteArrayOf('a'.toByte(), 'b'.toByte(), 'c'.toByte())
+ assertArrayEquals(expected, sink)
+ }
+ }
+
+ @Test fun readIntoByteArrayNotEnough() {
+ sink.writeUtf8("abcd")
+ sink.emit()
+
+ val sink = ByteArray(5)
+ val read = source.read(sink)
+ if (factory.isOneByteAtATime) {
+ assertEquals(1, read.toLong())
+ val expected = byteArrayOf('a'.toByte(), 0, 0, 0, 0)
+ assertArrayEquals(expected, sink)
+ } else {
+ assertEquals(4, read.toLong())
+ val expected = byteArrayOf('a'.toByte(), 'b'.toByte(), 'c'.toByte(), 'd'.toByte(), 0)
+ assertArrayEquals(expected, sink)
+ }
+ }
+
+ @Test fun readIntoByteArrayOffsetAndCount() {
+ sink.writeUtf8("abcd")
+ sink.emit()
+
+ val sink = ByteArray(7)
+ val read = source.read(sink, 2, 3)
+ if (factory.isOneByteAtATime) {
+ assertEquals(1, read.toLong())
+ val expected = byteArrayOf(0, 0, 'a'.toByte(), 0, 0, 0, 0)
+ assertArrayEquals(expected, sink)
+ } else {
+ assertEquals(3, read.toLong())
+ val expected = byteArrayOf(0, 0, 'a'.toByte(), 'b'.toByte(), 'c'.toByte(), 0, 0)
+ assertArrayEquals(expected, sink)
+ }
+ }
+
+ @Test fun readByteArray() {
+ val string = "abcd" + "e".repeat(Segment.SIZE)
+ sink.writeUtf8(string)
+ sink.emit()
+ assertArrayEquals(string.asUtf8ToByteArray(), source.readByteArray())
+ }
+
+ @Test fun readByteArrayPartial() {
+ sink.writeUtf8("abcd")
+ sink.emit()
+ assertEquals("[97, 98, 99]", source.readByteArray(3).contentToString())
+ assertEquals("d", source.readUtf8(1))
+ }
+
+ @Test fun readByteArrayTooShortThrows() {
+ sink.writeUtf8("abc")
+ sink.emit()
+ assertFailsWith<EOFException> {
+ source.readByteArray(4)
+ }
+
+ assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data.
+ }
+
+ @Test fun readByteString() {
+ sink.writeUtf8("abcd").writeUtf8("e".repeat(Segment.SIZE))
+ sink.emit()
+ assertEquals("abcd" + "e".repeat(Segment.SIZE), source.readByteString().utf8())
+ }
+
+ @Test fun readByteStringPartial() {
+ sink.writeUtf8("abcd").writeUtf8("e".repeat(Segment.SIZE))
+ sink.emit()
+ assertEquals("abc", source.readByteString(3).utf8())
+ assertEquals("d", source.readUtf8(1))
+ }
+
+ @Test fun readByteStringTooShortThrows() {
+ sink.writeUtf8("abc")
+ sink.emit()
+ assertFailsWith<EOFException> {
+ source.readByteString(4)
+ }
+
+ assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data.
+ }
+
+ @Test fun readUtf8SpansSegments() {
+ sink.writeUtf8("a".repeat(Segment.SIZE * 2))
+ sink.emit()
+ source.skip((Segment.SIZE - 1).toLong())
+ assertEquals("aa", source.readUtf8(2))
+ }
+
+ @Test fun readUtf8Segment() {
+ sink.writeUtf8("a".repeat(Segment.SIZE))
+ sink.emit()
+ assertEquals("a".repeat(Segment.SIZE), source.readUtf8(Segment.SIZE.toLong()))
+ }
+
+ @Test fun readUtf8PartialBuffer() {
+ sink.writeUtf8("a".repeat(Segment.SIZE + 20))
+ sink.emit()
+ assertEquals("a".repeat(Segment.SIZE + 10), source.readUtf8((Segment.SIZE + 10).toLong()))
+ }
+
+ @Test fun readUtf8EntireBuffer() {
+ sink.writeUtf8("a".repeat(Segment.SIZE * 2))
+ sink.emit()
+ assertEquals("a".repeat(Segment.SIZE * 2), source.readUtf8())
+ }
+
+ @Test fun readUtf8TooShortThrows() {
+ sink.writeUtf8("abc")
+ sink.emit()
+ assertFailsWith<EOFException> {
+ source.readUtf8(4L)
+ }
+
+ assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data.
+ }
+
+ @Test fun skip() {
+ sink.writeUtf8("a")
+ sink.writeUtf8("b".repeat(Segment.SIZE))
+ sink.writeUtf8("c")
+ sink.emit()
+ source.skip(1)
+ assertEquals('b'.toLong(), (source.readByte() and 0xff).toLong())
+ source.skip((Segment.SIZE - 2).toLong())
+ assertEquals('b'.toLong(), (source.readByte() and 0xff).toLong())
+ source.skip(1)
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun skipInsufficientData() {
+ sink.writeUtf8("a")
+ sink.emit()
+
+ assertFailsWith<EOFException> {
+ source.skip(2)
+ }
+ }
+
+ @Test fun indexOf() {
+ // The segment is empty.
+ assertEquals(-1, source.indexOf('a'.toByte()))
+
+ // The segment has one value.
+ sink.writeUtf8("a") // a
+ sink.emit()
+ assertEquals(0, source.indexOf('a'.toByte()))
+ assertEquals(-1, source.indexOf('b'.toByte()))
+
+ // The segment has lots of data.
+ sink.writeUtf8("b".repeat(Segment.SIZE - 2)) // ab...b
+ sink.emit()
+ assertEquals(0, source.indexOf('a'.toByte()))
+ assertEquals(1, source.indexOf('b'.toByte()))
+ assertEquals(-1, source.indexOf('c'.toByte()))
+
+ // The segment doesn't start at 0, it starts at 2.
+ source.skip(2) // b...b
+ assertEquals(-1, source.indexOf('a'.toByte()))
+ assertEquals(0, source.indexOf('b'.toByte()))
+ assertEquals(-1, source.indexOf('c'.toByte()))
+
+ // The segment is full.
+ sink.writeUtf8("c") // b...bc
+ sink.emit()
+ assertEquals(-1, source.indexOf('a'.toByte()))
+ assertEquals(0, source.indexOf('b'.toByte()))
+ assertEquals((Segment.SIZE - 3).toLong(), source.indexOf('c'.toByte()))
+
+ // The segment doesn't start at 2, it starts at 4.
+ source.skip(2) // b...bc
+ assertEquals(-1, source.indexOf('a'.toByte()))
+ assertEquals(0, source.indexOf('b'.toByte()))
+ assertEquals((Segment.SIZE - 5).toLong(), source.indexOf('c'.toByte()))
+
+ // Two segments.
+ sink.writeUtf8("d") // b...bcd, d is in the 2nd segment.
+ sink.emit()
+ assertEquals((Segment.SIZE - 4).toLong(), source.indexOf('d'.toByte()))
+ assertEquals(-1, source.indexOf('e'.toByte()))
+ }
+
+ @Test fun indexOfByteWithStartOffset() {
+ sink.writeUtf8("a").writeUtf8("b".repeat(Segment.SIZE)).writeUtf8("c")
+ sink.emit()
+ assertEquals(-1, source.indexOf('a'.toByte(), 1))
+ assertEquals(15, source.indexOf('b'.toByte(), 15))
+ }
+
+ @Test fun indexOfByteWithBothOffsets() {
+ if (factory.isOneByteAtATime) {
+ // When run on Travis this causes out-of-memory errors.
+ return
+ }
+ val a = 'a'.toByte()
+ val c = 'c'.toByte()
+
+ val size = Segment.SIZE * 5
+ val bytes = ByteArray(size) { a }
+
+ // These are tricky places where the buffer
+ // starts, ends, or segments come together.
+ val points = intArrayOf(
+ 0,
+ 1,
+ 2,
+ Segment.SIZE - 1,
+ Segment.SIZE,
+ Segment.SIZE + 1,
+ size / 2 - 1,
+ size / 2,
+ size / 2 + 1,
+ size - Segment.SIZE - 1,
+ size - Segment.SIZE,
+ size - Segment.SIZE + 1,
+ size - 3,
+ size - 2,
+ size - 1
+ )
+
+ // In each iteration, we write c to the known point and then search for it using different
+ // windows. Some of the windows don't overlap with c's position, and therefore a match shouldn't
+ // be found.
+ for (p in points) {
+ bytes[p] = c
+ sink.write(bytes)
+ sink.emit()
+
+ assertEquals(p.toLong(), source.indexOf(c, 0, size.toLong()))
+ assertEquals(p.toLong(), source.indexOf(c, 0, (p + 1).toLong()))
+ assertEquals(p.toLong(), source.indexOf(c, p.toLong(), size.toLong()))
+ assertEquals(p.toLong(), source.indexOf(c, p.toLong(), (p + 1).toLong()))
+ assertEquals(p.toLong(), source.indexOf(c, (p / 2).toLong(), (p * 2 + 1).toLong()))
+ assertEquals(-1, source.indexOf(c, 0, (p / 2).toLong()))
+ assertEquals(-1, source.indexOf(c, 0, p.toLong()))
+ assertEquals(-1, source.indexOf(c, 0, 0))
+ assertEquals(-1, source.indexOf(c, p.toLong(), p.toLong()))
+
+ // Reset.
+ source.readUtf8()
+ bytes[p] = a
+ }
+ }
+
+ @Test fun indexOfByteInvalidBoundsThrows() {
+ sink.writeUtf8("abc")
+ sink.emit()
+
+ try {
+ source.indexOf('a'.toByte(), -1)
+ fail("Expected failure: fromIndex < 0")
+ } catch (expected: IllegalArgumentException) {
+ }
+
+ try {
+ source.indexOf('a'.toByte(), 10, 0)
+ fail("Expected failure: fromIndex > toIndex")
+ } catch (expected: IllegalArgumentException) {
+ }
+ }
+
+ @Test fun indexOfByteString() {
+ assertEquals(-1, source.indexOf("flop".encodeUtf8()))
+
+ sink.writeUtf8("flip flop")
+ sink.emit()
+ assertEquals(5, source.indexOf("flop".encodeUtf8()))
+ source.readUtf8() // Clear stream.
+
+ // Make sure we backtrack and resume searching after partial match.
+ sink.writeUtf8("hi hi hi hey")
+ sink.emit()
+ assertEquals(3, source.indexOf("hi hi hey".encodeUtf8()))
+ }
+
+ @Test fun indexOfByteStringAtSegmentBoundary() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 1))
+ sink.writeUtf8("bcd")
+ sink.emit()
+ assertEquals(
+ (Segment.SIZE - 3).toLong(),
+ source.indexOf("aabc".encodeUtf8(), (Segment.SIZE - 4).toLong())
+ )
+ assertEquals(
+ (Segment.SIZE - 3).toLong(),
+ source.indexOf("aabc".encodeUtf8(), (Segment.SIZE - 3).toLong())
+ )
+ assertEquals(
+ (Segment.SIZE - 2).toLong(),
+ source.indexOf("abcd".encodeUtf8(), (Segment.SIZE - 2).toLong())
+ )
+ assertEquals(
+ (Segment.SIZE - 2).toLong(),
+ source.indexOf("abc".encodeUtf8(), (Segment.SIZE - 2).toLong())
+ )
+ assertEquals(
+ (Segment.SIZE - 2).toLong(),
+ source.indexOf("abc".encodeUtf8(), (Segment.SIZE - 2).toLong())
+ )
+ assertEquals(
+ (Segment.SIZE - 2).toLong(),
+ source.indexOf("ab".encodeUtf8(), (Segment.SIZE - 2).toLong())
+ )
+ assertEquals(
+ (Segment.SIZE - 2).toLong(),
+ source.indexOf("a".encodeUtf8(), (Segment.SIZE - 2).toLong())
+ )
+ assertEquals(
+ (Segment.SIZE - 1).toLong(),
+ source.indexOf("bc".encodeUtf8(), (Segment.SIZE - 2).toLong())
+ )
+ assertEquals(
+ (Segment.SIZE - 1).toLong(),
+ source.indexOf("b".encodeUtf8(), (Segment.SIZE - 2).toLong())
+ )
+ assertEquals(
+ Segment.SIZE.toLong(),
+ source.indexOf("c".encodeUtf8(), (Segment.SIZE - 2).toLong())
+ )
+ assertEquals(
+ Segment.SIZE.toLong(),
+ source.indexOf("c".encodeUtf8(), Segment.SIZE.toLong())
+ )
+ assertEquals(
+ (Segment.SIZE + 1).toLong(),
+ source.indexOf("d".encodeUtf8(), (Segment.SIZE - 2).toLong())
+ )
+ assertEquals(
+ (Segment.SIZE + 1).toLong(),
+ source.indexOf("d".encodeUtf8(), (Segment.SIZE + 1).toLong())
+ )
+ }
+
+ @Test fun indexOfDoesNotWrapAround() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 1))
+ sink.writeUtf8("bcd")
+ sink.emit()
+ assertEquals(-1, source.indexOf("abcda".encodeUtf8(), (Segment.SIZE - 3).toLong()))
+ }
+
+ @Test fun indexOfByteStringWithOffset() {
+ assertEquals(-1, source.indexOf("flop".encodeUtf8(), 1))
+
+ sink.writeUtf8("flop flip flop")
+ sink.emit()
+ assertEquals(10, source.indexOf("flop".encodeUtf8(), 1))
+ source.readUtf8() // Clear stream
+
+ // Make sure we backtrack and resume searching after partial match.
+ sink.writeUtf8("hi hi hi hi hey")
+ sink.emit()
+ assertEquals(6, source.indexOf("hi hi hey".encodeUtf8(), 1))
+ }
+
+ @Test fun indexOfByteStringInvalidArgumentsThrows() {
+ try {
+ source.indexOf(ByteString.of())
+ fail()
+ } catch (e: IllegalArgumentException) {
+ assertEquals("bytes is empty", e.message)
+ }
+
+ try {
+ source.indexOf("hi".encodeUtf8(), -1)
+ fail()
+ } catch (e: IllegalArgumentException) {
+ assertEquals("fromIndex < 0: -1", e.message)
+ }
+ }
+
+ /**
+ * With [BufferedSourceFactory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE], this code was extremely slow.
+ * https://github.com/square/okio/issues/171
+ */
+ @Test fun indexOfByteStringAcrossSegmentBoundaries() {
+ sink.writeUtf8("a".repeat(Segment.SIZE * 2 - 3))
+ sink.writeUtf8("bcdefg")
+ sink.emit()
+ assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("ab".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abc".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abcd".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abcde".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abcdef".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abcdefg".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 - 3).toLong(), source.indexOf("bcdefg".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 - 2).toLong(), source.indexOf("cdefg".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 - 1).toLong(), source.indexOf("defg".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2).toLong(), source.indexOf("efg".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 + 1).toLong(), source.indexOf("fg".encodeUtf8()))
+ assertEquals((Segment.SIZE * 2 + 2).toLong(), source.indexOf("g".encodeUtf8()))
+ }
+
+ @Test fun indexOfElement() {
+ sink.writeUtf8("a").writeUtf8("b".repeat(Segment.SIZE)).writeUtf8("c")
+ sink.emit()
+ assertEquals(0, source.indexOfElement("DEFGaHIJK".encodeUtf8()))
+ assertEquals(1, source.indexOfElement("DEFGHIJKb".encodeUtf8()))
+ assertEquals((Segment.SIZE + 1).toLong(), source.indexOfElement("cDEFGHIJK".encodeUtf8()))
+ assertEquals(1, source.indexOfElement("DEFbGHIc".encodeUtf8()))
+ assertEquals(-1L, source.indexOfElement("DEFGHIJK".encodeUtf8()))
+ assertEquals(-1L, source.indexOfElement("".encodeUtf8()))
+ }
+
+ @Test fun indexOfElementWithOffset() {
+ sink.writeUtf8("a").writeUtf8("b".repeat(Segment.SIZE)).writeUtf8("c")
+ sink.emit()
+ assertEquals(-1, source.indexOfElement("DEFGaHIJK".encodeUtf8(), 1))
+ assertEquals(15, source.indexOfElement("DEFGHIJKb".encodeUtf8(), 15))
+ }
+
+ @Test fun indexOfByteWithFromIndex() {
+ sink.writeUtf8("aaa")
+ sink.emit()
+ assertEquals(0, source.indexOf('a'.toByte()))
+ assertEquals(0, source.indexOf('a'.toByte(), 0))
+ assertEquals(1, source.indexOf('a'.toByte(), 1))
+ assertEquals(2, source.indexOf('a'.toByte(), 2))
+ }
+
+ @Test fun indexOfByteStringWithFromIndex() {
+ sink.writeUtf8("aaa")
+ sink.emit()
+ assertEquals(0, source.indexOf("a".encodeUtf8()))
+ assertEquals(0, source.indexOf("a".encodeUtf8(), 0))
+ assertEquals(1, source.indexOf("a".encodeUtf8(), 1))
+ assertEquals(2, source.indexOf("a".encodeUtf8(), 2))
+ }
+
+ @Test fun indexOfElementWithFromIndex() {
+ sink.writeUtf8("aaa")
+ sink.emit()
+ assertEquals(0, source.indexOfElement("a".encodeUtf8()))
+ assertEquals(0, source.indexOfElement("a".encodeUtf8(), 0))
+ assertEquals(1, source.indexOfElement("a".encodeUtf8(), 1))
+ assertEquals(2, source.indexOfElement("a".encodeUtf8(), 2))
+ }
+
+ @Test fun request() {
+ sink.writeUtf8("a").writeUtf8("b".repeat(Segment.SIZE)).writeUtf8("c")
+ sink.emit()
+ assertTrue(source.request((Segment.SIZE + 2).toLong()))
+ assertFalse(source.request((Segment.SIZE + 3).toLong()))
+ }
+
+ @Test fun require() {
+ sink.writeUtf8("a").writeUtf8("b".repeat(Segment.SIZE)).writeUtf8("c")
+ sink.emit()
+ source.require((Segment.SIZE + 2).toLong())
+ assertFailsWith<EOFException> {
+ source.require((Segment.SIZE + 3).toLong())
+ }
+ }
+
+ @Test fun longHexString() {
+ assertLongHexString("8000000000000000", Long.MIN_VALUE)
+ assertLongHexString("fffffffffffffffe", -0x2L)
+ assertLongHexString("FFFFFFFFFFFFFFFe", -0x2L)
+ assertLongHexString("ffffffffffffffff", -0x1L)
+ assertLongHexString("FFFFFFFFFFFFFFFF", -0x1L)
+ assertLongHexString("0000000000000000", 0x0L)
+ assertLongHexString("0000000000000001", 0x1L)
+ assertLongHexString("7999999999999999", 0x7999999999999999L)
+
+ assertLongHexString("FF", 0xFF)
+ assertLongHexString("0000000000000001", 0x1)
+ }
+
+ @Test fun hexStringWithManyLeadingZeros() {
+ assertLongHexString("00000000000000001", 0x1)
+ assertLongHexString("0000000000000000ffffffffffffffff", -0x1L)
+ assertLongHexString("00000000000000007fffffffffffffff", 0x7fffffffffffffffL)
+ assertLongHexString("0".repeat(Segment.SIZE + 1) + "1", 0x1)
+ }
+
+ private fun assertLongHexString(s: String, expected: Long) {
+ sink.writeUtf8(s)
+ sink.emit()
+ val actual = source.readHexadecimalUnsignedLong()
+ assertEquals(expected, actual, "$s --> $expected")
+ }
+
+ @Test fun longHexStringAcrossSegment() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 8)).writeUtf8("FFFFFFFFFFFFFFFF")
+ sink.emit()
+ source.skip((Segment.SIZE - 8).toLong())
+ assertEquals(-1, source.readHexadecimalUnsignedLong())
+ }
+
+ @Test fun longHexStringTooLongThrows() {
+ try {
+ sink.writeUtf8("fffffffffffffffff")
+ sink.emit()
+ source.readHexadecimalUnsignedLong()
+ fail()
+ } catch (e: NumberFormatException) {
+ assertEquals("Number too large: fffffffffffffffff", e.message)
+ }
+ }
+
+ @Test fun longHexStringTooShortThrows() {
+ try {
+ sink.writeUtf8(" ")
+ sink.emit()
+ source.readHexadecimalUnsignedLong()
+ fail()
+ } catch (e: NumberFormatException) {
+ assertEquals("Expected leading [0-9a-fA-F] character but was 0x20", e.message)
+ }
+ }
+
+ @Test fun longHexEmptySourceThrows() {
+ try {
+ sink.writeUtf8("")
+ sink.emit()
+ source.readHexadecimalUnsignedLong()
+ fail()
+ } catch (expected: EOFException) {
+ }
+ }
+
+ @Test fun longDecimalString() {
+ assertLongDecimalString("-9223372036854775808", Long.MIN_VALUE)
+ assertLongDecimalString("-1", -1L)
+ assertLongDecimalString("0", 0L)
+ assertLongDecimalString("1", 1L)
+ assertLongDecimalString("9223372036854775807", Long.MAX_VALUE)
+
+ assertLongDecimalString("00000001", 1L)
+ assertLongDecimalString("-000001", -1L)
+ }
+
+ private fun assertLongDecimalString(s: String, expected: Long) {
+ sink.writeUtf8(s)
+ sink.writeUtf8("zzz")
+ sink.emit()
+ val actual = source.readDecimalLong()
+ assertEquals(expected, actual, "$s --> $expected")
+ assertEquals("zzz", source.readUtf8())
+ }
+
+ @Test fun longDecimalStringAcrossSegment() {
+ sink.writeUtf8("a".repeat(Segment.SIZE - 8)).writeUtf8("1234567890123456")
+ sink.writeUtf8("zzz")
+ sink.emit()
+ source.skip((Segment.SIZE - 8).toLong())
+ assertEquals(1234567890123456L, source.readDecimalLong())
+ assertEquals("zzz", source.readUtf8())
+ }
+
+ @Test fun longDecimalStringTooLongThrows() {
+ try {
+ sink.writeUtf8("12345678901234567890") // Too many digits.
+ sink.emit()
+ source.readDecimalLong()
+ fail()
+ } catch (e: NumberFormatException) {
+ assertEquals("Number too large: 12345678901234567890", e.message)
+ }
+ }
+
+ @Test fun longDecimalStringTooHighThrows() {
+ try {
+ sink.writeUtf8("9223372036854775808") // Right size but cannot fit.
+ sink.emit()
+ source.readDecimalLong()
+ fail()
+ } catch (e: NumberFormatException) {
+ assertEquals("Number too large: 9223372036854775808", e.message)
+ }
+ }
+
+ @Test fun longDecimalStringTooLowThrows() {
+ try {
+ sink.writeUtf8("-9223372036854775809") // Right size but cannot fit.
+ sink.emit()
+ source.readDecimalLong()
+ fail()
+ } catch (e: NumberFormatException) {
+ assertEquals("Number too large: -9223372036854775809", e.message)
+ }
+ }
+
+ @Test fun longDecimalStringTooShortThrows() {
+ try {
+ sink.writeUtf8(" ")
+ sink.emit()
+ source.readDecimalLong()
+ fail()
+ } catch (e: NumberFormatException) {
+ assertEquals("Expected leading [0-9] or '-' character but was 0x20", e.message)
+ }
+ }
+
+ @Test fun longDecimalEmptyThrows() {
+ try {
+ sink.writeUtf8("")
+ sink.emit()
+ source.readDecimalLong()
+ fail()
+ } catch (expected: EOFException) {
+ }
+ }
+
+ @Test fun codePoints() {
+ sink.write("7f".decodeHex())
+ sink.emit()
+ assertEquals(0x7f, source.readUtf8CodePoint().toLong())
+
+ sink.write("dfbf".decodeHex())
+ sink.emit()
+ assertEquals(0x07ff, source.readUtf8CodePoint().toLong())
+
+ sink.write("efbfbf".decodeHex())
+ sink.emit()
+ assertEquals(0xffff, source.readUtf8CodePoint().toLong())
+
+ sink.write("f48fbfbf".decodeHex())
+ sink.emit()
+ assertEquals(0x10ffff, source.readUtf8CodePoint().toLong())
+ }
+
+ @Test fun decimalStringWithManyLeadingZeros() {
+ assertLongDecimalString("00000000000000001", 1)
+ assertLongDecimalString("00000000000000009223372036854775807", Long.MAX_VALUE)
+ assertLongDecimalString("-00000000000000009223372036854775808", Long.MIN_VALUE)
+ assertLongDecimalString("0".repeat(Segment.SIZE + 1) + "1", 1)
+ }
+
+ @Test fun select() {
+ val options = Options.of(
+ "ROCK".encodeUtf8(),
+ "SCISSORS".encodeUtf8(),
+ "PAPER".encodeUtf8()
+ )
+
+ sink.writeUtf8("PAPER,SCISSORS,ROCK")
+ sink.emit()
+ assertEquals(2, source.select(options).toLong())
+ assertEquals(','.toLong(), source.readByte().toLong())
+ assertEquals(1, source.select(options).toLong())
+ assertEquals(','.toLong(), source.readByte().toLong())
+ assertEquals(0, source.select(options).toLong())
+ assertTrue(source.exhausted())
+ }
+
+ /** Note that this test crashes the VM on Android. */
+ @Test fun selectSpanningMultipleSegments() {
+ val commonPrefix = randomBytes(Segment.SIZE + 10)
+ val a = Buffer().write(commonPrefix).writeUtf8("a").readByteString()
+ val bc = Buffer().write(commonPrefix).writeUtf8("bc").readByteString()
+ val bd = Buffer().write(commonPrefix).writeUtf8("bd").readByteString()
+ val options = Options.of(a, bc, bd)
+
+ sink.write(bd)
+ sink.write(a)
+ sink.write(bc)
+ sink.emit()
+
+ assertEquals(2, source.select(options).toLong())
+ assertEquals(0, source.select(options).toLong())
+ assertEquals(1, source.select(options).toLong())
+ assertTrue(source.exhausted())
+ }
+
+ @Test fun selectNotFound() {
+ val options = Options.of(
+ "ROCK".encodeUtf8(),
+ "SCISSORS".encodeUtf8(),
+ "PAPER".encodeUtf8()
+ )
+
+ sink.writeUtf8("SPOCK")
+ sink.emit()
+ assertEquals(-1, source.select(options).toLong())
+ assertEquals("SPOCK", source.readUtf8())
+ }
+
+ @Test fun selectValuesHaveCommonPrefix() {
+ val options = Options.of(
+ "abcd".encodeUtf8(),
+ "abce".encodeUtf8(),
+ "abcc".encodeUtf8()
+ )
+
+ sink.writeUtf8("abcc").writeUtf8("abcd").writeUtf8("abce")
+ sink.emit()
+ assertEquals(2, source.select(options).toLong())
+ assertEquals(0, source.select(options).toLong())
+ assertEquals(1, source.select(options).toLong())
+ }
+
+ @Test fun selectLongerThanSource() {
+ val options = Options.of(
+ "abcd".encodeUtf8(),
+ "abce".encodeUtf8(),
+ "abcc".encodeUtf8()
+ )
+ sink.writeUtf8("abc")
+ sink.emit()
+ assertEquals(-1, source.select(options).toLong())
+ assertEquals("abc", source.readUtf8())
+ }
+
+ @Test fun selectReturnsFirstByteStringThatMatches() {
+ val options = Options.of(
+ "abcd".encodeUtf8(),
+ "abc".encodeUtf8(),
+ "abcde".encodeUtf8()
+ )
+ sink.writeUtf8("abcdef")
+ sink.emit()
+ assertEquals(0, source.select(options).toLong())
+ assertEquals("ef", source.readUtf8())
+ }
+
+ @Test fun selectFromEmptySource() {
+ val options = Options.of(
+ "abc".encodeUtf8(),
+ "def".encodeUtf8()
+ )
+ assertEquals(-1, source.select(options).toLong())
+ }
+
+ @Test fun selectNoByteStringsFromEmptySource() {
+ val options = Options.of()
+ assertEquals(-1, source.select(options).toLong())
+ }
+
+ @Test fun peek() {
+ sink.writeUtf8("abcdefghi")
+ sink.emit()
+
+ assertEquals("abc", source.readUtf8(3))
+
+ val peek = source.peek()
+ assertEquals("def", peek.readUtf8(3))
+ assertEquals("ghi", peek.readUtf8(3))
+ assertFalse(peek.request(1))
+
+ assertEquals("def", source.readUtf8(3))
+ }
+
+ @Test fun peekMultiple() {
+ sink.writeUtf8("abcdefghi")
+ sink.emit()
+
+ assertEquals("abc", source.readUtf8(3))
+
+ val peek1 = source.peek()
+ val peek2 = source.peek()
+
+ assertEquals("def", peek1.readUtf8(3))
+
+ assertEquals("def", peek2.readUtf8(3))
+ assertEquals("ghi", peek2.readUtf8(3))
+ assertFalse(peek2.request(1))
+
+ assertEquals("ghi", peek1.readUtf8(3))
+ assertFalse(peek1.request(1))
+
+ assertEquals("def", source.readUtf8(3))
+ }
+
+ @Test fun peekLarge() {
+ sink.writeUtf8("abcdef")
+ sink.writeUtf8("g".repeat(2 * Segment.SIZE))
+ sink.writeUtf8("hij")
+ sink.emit()
+
+ assertEquals("abc", source.readUtf8(3))
+
+ val peek = source.peek()
+ assertEquals("def", peek.readUtf8(3))
+ peek.skip((2 * Segment.SIZE).toLong())
+ assertEquals("hij", peek.readUtf8(3))
+ assertFalse(peek.request(1))
+
+ assertEquals("def", source.readUtf8(3))
+ source.skip((2 * Segment.SIZE).toLong())
+ assertEquals("hij", source.readUtf8(3))
+ }
+
+ @Test fun peekInvalid() {
+ sink.writeUtf8("abcdefghi")
+ sink.emit()
+
+ assertEquals("abc", source.readUtf8(3))
+
+ val peek = source.peek()
+ assertEquals("def", peek.readUtf8(3))
+ assertEquals("ghi", peek.readUtf8(3))
+ assertFalse(peek.request(1))
+
+ assertEquals("def", source.readUtf8(3))
+
+ try {
+ peek.readUtf8()
+ fail()
+ } catch (e: IllegalStateException) {
+ assertEquals("Peek source is invalid because upstream source was used", e.message)
+ }
+ }
+
+ @Test fun peekSegmentThenInvalid() {
+ sink.writeUtf8("abc")
+ sink.writeUtf8("d".repeat(2 * Segment.SIZE))
+ sink.emit()
+
+ assertEquals("abc", source.readUtf8(3))
+
+ // Peek a little data and skip the rest of the upstream source
+ val peek = source.peek()
+ assertEquals("ddd", peek.readUtf8(3))
+ source.readAll(blackholeSink())
+
+ // Skip the rest of the buffered data
+ peek.skip(peek.buffer.size)
+
+ try {
+ peek.readByte()
+ fail()
+ } catch (e: IllegalStateException) {
+ assertEquals("Peek source is invalid because upstream source was used", e.message)
+ }
+ }
+
+ @Test fun peekDoesntReadTooMuch() {
+ // 6 bytes in source's buffer plus 3 bytes upstream.
+ sink.writeUtf8("abcdef")
+ sink.emit()
+ source.require(6L)
+ sink.writeUtf8("ghi")
+ sink.emit()
+
+ val peek = source.peek()
+
+ // Read 3 bytes. This reads some of the buffered data.
+ assertTrue(peek.request(3))
+ if (source !is Buffer) {
+ assertEquals(6, source.buffer.size)
+ assertEquals(6, peek.buffer.size)
+ }
+ assertEquals("abc", peek.readUtf8(3L))
+
+ // Read 3 more bytes. This exhausts the buffered data.
+ assertTrue(peek.request(3))
+ if (source !is Buffer) {
+ assertEquals(6, source.buffer.size)
+ assertEquals(3, peek.buffer.size)
+ }
+ assertEquals("def", peek.readUtf8(3L))
+
+ // Read 3 more bytes. This draws new bytes.
+ assertTrue(peek.request(3))
+ assertEquals(9, source.buffer.size)
+ assertEquals(3, peek.buffer.size)
+ assertEquals("ghi", peek.readUtf8(3L))
+ }
+
+ @Test fun rangeEquals() {
+ sink.writeUtf8("A man, a plan, a canal. Panama.")
+ sink.emit()
+ assertTrue(source.rangeEquals(7, "a plan".encodeUtf8()))
+ assertTrue(source.rangeEquals(0, "A man".encodeUtf8()))
+ assertTrue(source.rangeEquals(24, "Panama".encodeUtf8()))
+ assertFalse(source.rangeEquals(24, "Panama. Panama. Panama.".encodeUtf8()))
+ }
+
+ @Test fun rangeEqualsWithOffsetAndCount() {
+ sink.writeUtf8("A man, a plan, a canal. Panama.")
+ sink.emit()
+ assertTrue(source.rangeEquals(7, "aaa plannn".encodeUtf8(), 2, 6))
+ assertTrue(source.rangeEquals(0, "AAA mannn".encodeUtf8(), 2, 5))
+ assertTrue(source.rangeEquals(24, "PPPanamaaa".encodeUtf8(), 2, 6))
+ }
+
+ @Test fun rangeEqualsOnlyReadsUntilMismatch() {
+ if (factory !== BufferedSourceFactory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE) return // Other sources read in chunks anyway.
+
+ sink.writeUtf8("A man, a plan, a canal. Panama.")
+ sink.emit()
+ assertFalse(source.rangeEquals(0, ("A man.").encodeUtf8()))
+ assertEquals("A man,", source.buffer.readUtf8())
+ }
+
+ @Test fun rangeEqualsArgumentValidation() {
+ // Negative source offset.
+ assertFalse(source.rangeEquals(-1, "A".encodeUtf8()))
+ // Negative bytes offset.
+ assertFalse(source.rangeEquals(0, "A".encodeUtf8(), -1, 1))
+ // Bytes offset longer than bytes length.
+ assertFalse(source.rangeEquals(0, "A".encodeUtf8(), 2, 1))
+ // Negative byte count.
+ assertFalse(source.rangeEquals(0, "A".encodeUtf8(), 0, -1))
+ // Byte count longer than bytes length.
+ assertFalse(source.rangeEquals(0, "A".encodeUtf8(), 0, 2))
+ // Bytes offset plus byte count longer than bytes length.
+ assertFalse(source.rangeEquals(0, "A".encodeUtf8(), 1, 1))
+ }
+
+ @Test fun factorySegmentSizes() {
+ sink.writeUtf8("abc")
+ sink.emit()
+ source.require(3)
+ if (factory.isOneByteAtATime) {
+ assertEquals(listOf(1, 1, 1), segmentSizes(source.buffer))
+ } else {
+ assertEquals(listOf(3), segmentSizes(source.buffer))
+ }
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/BufferCommonTest.kt b/okio/src/commonTest/kotlin/okio/BufferCommonTest.kt
new file mode 100644
index 00000000..842faffe
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/BufferCommonTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.ByteString.Companion.encodeUtf8
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class BufferCommonTest {
+
+ @Test fun copyToBuffer() {
+ val source = Buffer()
+ source.write("party".encodeUtf8())
+
+ val target = Buffer()
+ source.copyTo(target)
+ assertEquals("party", target.readByteString().utf8())
+ assertEquals("party", source.readByteString().utf8())
+ }
+
+ @Test fun copyToBufferWithOffset() {
+ val source = Buffer()
+ source.write("party".encodeUtf8())
+
+ val target = Buffer()
+ source.copyTo(target, 2)
+ assertEquals("rty", target.readByteString().utf8())
+ assertEquals("party", source.readByteString().utf8())
+ }
+
+ @Test fun copyToBufferWithByteCount() {
+ val source = Buffer()
+ source.write("party".encodeUtf8())
+
+ val target = Buffer()
+ source.copyTo(target, 0, 3)
+ assertEquals("par", target.readByteString().utf8())
+ assertEquals("party", source.readByteString().utf8())
+ }
+
+ @Test fun copyToBufferWithOffsetAndByteCount() {
+ val source = Buffer()
+ source.write("party".encodeUtf8())
+
+ val target = Buffer()
+ source.copyTo(target, 1, 3)
+ assertEquals("art", target.readByteString().utf8())
+ assertEquals("party", source.readByteString().utf8())
+ }
+
+ @Test fun completeSegmentByteCountOnEmptyBuffer() {
+ val buffer = Buffer()
+ assertEquals(0, buffer.completeSegmentByteCount())
+ }
+
+ @Test fun completeSegmentByteCountOnBufferWithFullSegments() {
+ val buffer = Buffer()
+ buffer.writeUtf8("a".repeat(Segment.SIZE * 4))
+ assertEquals((Segment.SIZE * 4).toLong(), buffer.completeSegmentByteCount())
+ }
+
+ @Test fun completeSegmentByteCountOnBufferWithIncompleteTailSegment() {
+ val buffer = Buffer()
+ buffer.writeUtf8("a".repeat(Segment.SIZE * 4 - 10))
+ assertEquals((Segment.SIZE * 3).toLong(), buffer.completeSegmentByteCount())
+ }
+
+ @Test fun testHash() {
+ val buffer = Buffer().apply { write("Kevin".encodeUtf8()) }
+ with(buffer) {
+ assertEquals("e043899daa0c7add37bc99792b2c045d6abbc6dc", sha1().hex())
+ assertEquals("f1cd318e412b5f7226e5f377a9544ff7", md5().hex())
+ assertEquals("0e4dd66217fc8d2e298b78c8cd9392870dcd065d0ff675d0edff5bcd227837e9", sha256().hex())
+ assertEquals("483676b93c4417198b465083d196ec6a9fab8d004515874b8ff47e041f5f56303cc08179625030b8b5b721c09149a18f0f59e64e7ae099518cea78d3d83167e1", sha512().hex())
+ }
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/BufferedSinkFactory.kt b/okio/src/commonTest/kotlin/okio/BufferedSinkFactory.kt
new file mode 100644
index 00000000..8f4f29ae
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/BufferedSinkFactory.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+internal interface BufferedSinkFactory {
+
+ fun create(data: Buffer): BufferedSink
+
+ companion object {
+ val BUFFER: BufferedSinkFactory = object : BufferedSinkFactory {
+ override fun create(data: Buffer): BufferedSink {
+ return data
+ }
+ }
+
+ val REAL_BUFFERED_SINK: BufferedSinkFactory = object : BufferedSinkFactory {
+ override fun create(data: Buffer): BufferedSink {
+ return (data as Sink).buffer()
+ }
+ }
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/BufferedSourceFactory.kt b/okio/src/commonTest/kotlin/okio/BufferedSourceFactory.kt
new file mode 100644
index 00000000..b9836202
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/BufferedSourceFactory.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+interface BufferedSourceFactory {
+ class Pipe(
+ var sink: BufferedSink,
+ var source: BufferedSource
+ )
+
+ val isOneByteAtATime: Boolean
+
+ fun pipe(): Pipe
+
+ companion object {
+ val BUFFER: BufferedSourceFactory = object : BufferedSourceFactory {
+
+ override val isOneByteAtATime: Boolean
+ get() = false
+
+ override fun pipe(): Pipe {
+ val buffer = Buffer()
+ return Pipe(
+ buffer,
+ buffer
+ )
+ }
+ }
+
+ val REAL_BUFFERED_SOURCE: BufferedSourceFactory = object :
+ BufferedSourceFactory {
+
+ override val isOneByteAtATime: Boolean
+ get() = false
+
+ override fun pipe(): Pipe {
+ val buffer = Buffer()
+ return Pipe(
+ buffer,
+ (buffer as Source).buffer()
+ )
+ }
+ }
+
+ /**
+ * A factory deliberately written to create buffers whose internal segments are always 1 byte
+ * long. We like testing with these segments because are likely to trigger bugs!
+ */
+ val ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE: BufferedSourceFactory = object :
+ BufferedSourceFactory {
+
+ override val isOneByteAtATime: Boolean
+ get() = true
+
+ override fun pipe(): Pipe {
+ val buffer = Buffer()
+ return Pipe(
+ buffer,
+ object : Source by buffer {
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ // Read one byte into a new buffer, then clone it so that the segment is shared.
+ // Shared segments cannot be compacted so we'll get a long chain of short segments.
+ val box = Buffer()
+ val result = buffer.read(box, minOf(byteCount, 1L))
+ if (result > 0L) sink.write(box.copy(), result)
+ return result
+ }
+ }.buffer()
+ )
+ }
+ }
+
+ val ONE_BYTE_AT_A_TIME_BUFFER: BufferedSourceFactory = object :
+ BufferedSourceFactory {
+
+ override val isOneByteAtATime: Boolean
+ get() = true
+
+ override fun pipe(): Pipe {
+ val buffer = Buffer()
+ return Pipe(
+ object : Sink by buffer {
+ override fun write(source: Buffer, byteCount: Long) {
+ // Write each byte into a new buffer, then clone it so that the segments are shared.
+ // Shared segments cannot be compacted so we'll get a long chain of short segments.
+ for (i in 0 until byteCount) {
+ val box = Buffer()
+ box.write(source, 1)
+ buffer.write(box.copy(), 1)
+ }
+ }
+ }.buffer(),
+ buffer
+ )
+ }
+ }
+
+ val PEEK_BUFFER: BufferedSourceFactory = object : BufferedSourceFactory {
+
+ override val isOneByteAtATime: Boolean
+ get() = false
+
+ override fun pipe(): Pipe {
+ val buffer = Buffer()
+ return Pipe(
+ buffer,
+ buffer.peek()
+ )
+ }
+ }
+
+ val PEEK_BUFFERED_SOURCE: BufferedSourceFactory = object :
+ BufferedSourceFactory {
+
+ override val isOneByteAtATime: Boolean
+ get() = false
+
+ override fun pipe(): Pipe {
+ val buffer = Buffer()
+ return Pipe(
+ buffer,
+ (buffer as Source).buffer().peek()
+ )
+ }
+ }
+
+ val PARAMETERIZED_TEST_VALUES = mutableListOf<Array<Any>>(
+ arrayOf(BUFFER),
+ arrayOf(REAL_BUFFERED_SOURCE),
+ arrayOf(ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE),
+ arrayOf(ONE_BYTE_AT_A_TIME_BUFFER),
+ arrayOf(PEEK_BUFFER),
+ arrayOf(PEEK_BUFFERED_SOURCE)
+ )
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/ByteStringFactory.kt b/okio/src/commonTest/kotlin/okio/ByteStringFactory.kt
new file mode 100644
index 00000000..bbf6cc6d
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/ByteStringFactory.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.ByteString.Companion.decodeHex
+import okio.ByteString.Companion.encodeUtf8
+import okio.internal.commonAsUtf8ToByteArray
+
+internal interface ByteStringFactory {
+ fun decodeHex(hex: String): ByteString
+
+ fun encodeUtf8(s: String): ByteString
+
+ companion object {
+ val BYTE_STRING: ByteStringFactory = object : ByteStringFactory {
+ override fun decodeHex(hex: String) = hex.decodeHex()
+ override fun encodeUtf8(s: String) = s.encodeUtf8()
+ }
+
+ val SEGMENTED_BYTE_STRING: ByteStringFactory = object : ByteStringFactory {
+ override fun decodeHex(hex: String) = Buffer().apply { write(hex.decodeHex()) }.snapshot()
+ override fun encodeUtf8(s: String) = Buffer().apply { writeUtf8(s) }.snapshot()
+ }
+
+ val ONE_BYTE_PER_SEGMENT: ByteStringFactory = object : ByteStringFactory {
+ override fun decodeHex(hex: String) = makeSegments(hex.decodeHex())
+ override fun encodeUtf8(s: String) = makeSegments(s.encodeUtf8())
+ }
+
+ // For Kotlin/JVM, the native Java UTF-8 encoder is used. This forces
+ // testing of the Okio encoder used for Kotlin/JS and Kotlin/Native to be
+ // tested on JVM as well.
+ val OKIO_ENCODER: ByteStringFactory = object : ByteStringFactory {
+ override fun decodeHex(hex: String) = hex.decodeHex()
+ override fun encodeUtf8(s: String) =
+ ByteString.of(*s.commonAsUtf8ToByteArray())
+ }
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/ByteStringTest.kt b/okio/src/commonTest/kotlin/okio/ByteStringTest.kt
new file mode 100644
index 00000000..c75c4581
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/ByteStringTest.kt
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.ByteString.Companion.decodeBase64
+import okio.ByteString.Companion.decodeHex
+import okio.ByteString.Companion.encodeUtf8
+import okio.internal.commonAsUtf8ToByteArray
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+class ByteStringTest : AbstractByteStringTest(ByteStringFactory.BYTE_STRING)
+class SegmentedByteStringTest : AbstractByteStringTest(ByteStringFactory.SEGMENTED_BYTE_STRING)
+class ByteStringOneBytePerSegmentTest : AbstractByteStringTest(ByteStringFactory.ONE_BYTE_PER_SEGMENT)
+class OkioEncoderTest : AbstractByteStringTest(ByteStringFactory.OKIO_ENCODER)
+
+abstract class AbstractByteStringTest internal constructor(
+ private val factory: ByteStringFactory
+) {
+ @Test fun get() {
+ val actual = factory.encodeUtf8("abc")
+ assertEquals(3, actual.size)
+ assertEquals(actual[0], 'a'.toByte())
+ assertEquals(actual[1], 'b'.toByte())
+ assertEquals(actual[2], 'c'.toByte())
+ try {
+ actual[-1]
+ fail("no index out of bounds: -1")
+ } catch (expected: IndexOutOfBoundsException) {
+ }
+ try {
+ actual[3]
+ fail("no index out of bounds: 3")
+ } catch (expected: IndexOutOfBoundsException) {
+ }
+ }
+
+ @Test fun getByte() {
+ val byteString = factory.decodeHex("ab12")
+ assertEquals(-85, byteString[0].toLong())
+ assertEquals(18, byteString[1].toLong())
+ }
+
+ @Test fun startsWithByteString() {
+ val byteString = factory.decodeHex("112233")
+ assertTrue(byteString.startsWith("".decodeHex()))
+ assertTrue(byteString.startsWith("11".decodeHex()))
+ assertTrue(byteString.startsWith("1122".decodeHex()))
+ assertTrue(byteString.startsWith("112233".decodeHex()))
+ assertFalse(byteString.startsWith("2233".decodeHex()))
+ assertFalse(byteString.startsWith("11223344".decodeHex()))
+ assertFalse(byteString.startsWith("112244".decodeHex()))
+ }
+
+ @Test fun endsWithByteString() {
+ val byteString = factory.decodeHex("112233")
+ assertTrue(byteString.endsWith("".decodeHex()))
+ assertTrue(byteString.endsWith("33".decodeHex()))
+ assertTrue(byteString.endsWith("2233".decodeHex()))
+ assertTrue(byteString.endsWith("112233".decodeHex()))
+ assertFalse(byteString.endsWith("1122".decodeHex()))
+ assertFalse(byteString.endsWith("00112233".decodeHex()))
+ assertFalse(byteString.endsWith("002233".decodeHex()))
+ }
+
+ @Test fun startsWithByteArray() {
+ val byteString = factory.decodeHex("112233")
+ assertTrue(byteString.startsWith("".decodeHex().toByteArray()))
+ assertTrue(byteString.startsWith("11".decodeHex().toByteArray()))
+ assertTrue(byteString.startsWith("1122".decodeHex().toByteArray()))
+ assertTrue(byteString.startsWith("112233".decodeHex().toByteArray()))
+ assertFalse(byteString.startsWith("2233".decodeHex().toByteArray()))
+ assertFalse(byteString.startsWith("11223344".decodeHex().toByteArray()))
+ assertFalse(byteString.startsWith("112244".decodeHex().toByteArray()))
+ }
+
+ @Test fun endsWithByteArray() {
+ val byteString = factory.decodeHex("112233")
+ assertTrue(byteString.endsWith("".decodeHex().toByteArray()))
+ assertTrue(byteString.endsWith("33".decodeHex().toByteArray()))
+ assertTrue(byteString.endsWith("2233".decodeHex().toByteArray()))
+ assertTrue(byteString.endsWith("112233".decodeHex().toByteArray()))
+ assertFalse(byteString.endsWith("1122".decodeHex().toByteArray()))
+ assertFalse(byteString.endsWith("00112233".decodeHex().toByteArray()))
+ assertFalse(byteString.endsWith("002233".decodeHex().toByteArray()))
+ }
+
+ @Test fun indexOfByteString() {
+ val byteString = factory.decodeHex("112233")
+ assertEquals(0, byteString.indexOf("112233".decodeHex()).toLong())
+ assertEquals(0, byteString.indexOf("1122".decodeHex()).toLong())
+ assertEquals(0, byteString.indexOf("11".decodeHex()).toLong())
+ assertEquals(0, byteString.indexOf("11".decodeHex(), 0).toLong())
+ assertEquals(0, byteString.indexOf("".decodeHex()).toLong())
+ assertEquals(0, byteString.indexOf("".decodeHex(), 0).toLong())
+ assertEquals(1, byteString.indexOf("2233".decodeHex()).toLong())
+ assertEquals(1, byteString.indexOf("22".decodeHex()).toLong())
+ assertEquals(1, byteString.indexOf("22".decodeHex(), 1).toLong())
+ assertEquals(1, byteString.indexOf("".decodeHex(), 1).toLong())
+ assertEquals(2, byteString.indexOf("33".decodeHex()).toLong())
+ assertEquals(2, byteString.indexOf("33".decodeHex(), 2).toLong())
+ assertEquals(2, byteString.indexOf("".decodeHex(), 2).toLong())
+ assertEquals(3, byteString.indexOf("".decodeHex(), 3).toLong())
+ assertEquals(-1, byteString.indexOf("112233".decodeHex(), 1).toLong())
+ assertEquals(-1, byteString.indexOf("44".decodeHex()).toLong())
+ assertEquals(-1, byteString.indexOf("11223344".decodeHex()).toLong())
+ assertEquals(-1, byteString.indexOf("112244".decodeHex()).toLong())
+ assertEquals(-1, byteString.indexOf("112233".decodeHex(), 1).toLong())
+ assertEquals(-1, byteString.indexOf("2233".decodeHex(), 2).toLong())
+ assertEquals(-1, byteString.indexOf("33".decodeHex(), 3).toLong())
+ assertEquals(-1, byteString.indexOf("".decodeHex(), 4).toLong())
+ }
+
+ @Test fun indexOfWithOffset() {
+ val byteString = factory.decodeHex("112233112233")
+ assertEquals(0, byteString.indexOf("112233".decodeHex(), -1).toLong())
+ assertEquals(0, byteString.indexOf("112233".decodeHex(), 0).toLong())
+ assertEquals(0, byteString.indexOf("112233".decodeHex()).toLong())
+ assertEquals(3, byteString.indexOf("112233".decodeHex(), 1).toLong())
+ assertEquals(3, byteString.indexOf("112233".decodeHex(), 2).toLong())
+ assertEquals(3, byteString.indexOf("112233".decodeHex(), 3).toLong())
+ assertEquals(-1, byteString.indexOf("112233".decodeHex(), 4).toLong())
+ }
+
+ @Test fun indexOfByteArray() {
+ val byteString = factory.decodeHex("112233")
+ assertEquals(0, byteString.indexOf("112233".decodeHex().toByteArray()).toLong())
+ assertEquals(1, byteString.indexOf("2233".decodeHex().toByteArray()).toLong())
+ assertEquals(2, byteString.indexOf("33".decodeHex().toByteArray()).toLong())
+ assertEquals(-1, byteString.indexOf("112244".decodeHex().toByteArray()).toLong())
+ }
+
+ @Test fun lastIndexOfByteString() {
+ val byteString = factory.decodeHex("112233")
+ assertEquals(0, byteString.lastIndexOf("112233".decodeHex()).toLong())
+ assertEquals(0, byteString.lastIndexOf("1122".decodeHex()).toLong())
+ assertEquals(0, byteString.lastIndexOf("11".decodeHex()).toLong())
+ assertEquals(0, byteString.lastIndexOf("11".decodeHex(), 3).toLong())
+ assertEquals(0, byteString.lastIndexOf("11".decodeHex(), 0).toLong())
+ assertEquals(0, byteString.lastIndexOf("".decodeHex(), 0).toLong())
+ assertEquals(1, byteString.lastIndexOf("2233".decodeHex()).toLong())
+ assertEquals(1, byteString.lastIndexOf("22".decodeHex()).toLong())
+ assertEquals(1, byteString.lastIndexOf("22".decodeHex(), 3).toLong())
+ assertEquals(1, byteString.lastIndexOf("22".decodeHex(), 1).toLong())
+ assertEquals(1, byteString.lastIndexOf("".decodeHex(), 1).toLong())
+ assertEquals(2, byteString.lastIndexOf("33".decodeHex()).toLong())
+ assertEquals(2, byteString.lastIndexOf("33".decodeHex(), 3).toLong())
+ assertEquals(2, byteString.lastIndexOf("33".decodeHex(), 2).toLong())
+ assertEquals(2, byteString.lastIndexOf("".decodeHex(), 2).toLong())
+ assertEquals(3, byteString.lastIndexOf("".decodeHex(), 3).toLong())
+ assertEquals(3, byteString.lastIndexOf("".decodeHex()).toLong())
+ assertEquals(-1, byteString.lastIndexOf("112233".decodeHex(), -1).toLong())
+ assertEquals(-1, byteString.lastIndexOf("112233".decodeHex(), -2).toLong())
+ assertEquals(-1, byteString.lastIndexOf("44".decodeHex()).toLong())
+ assertEquals(-1, byteString.lastIndexOf("11223344".decodeHex()).toLong())
+ assertEquals(-1, byteString.lastIndexOf("112244".decodeHex()).toLong())
+ assertEquals(-1, byteString.lastIndexOf("2233".decodeHex(), 0).toLong())
+ assertEquals(-1, byteString.lastIndexOf("33".decodeHex(), 1).toLong())
+ assertEquals(-1, byteString.lastIndexOf("".decodeHex(), -1).toLong())
+ }
+
+ @Test fun lastIndexOfByteArray() {
+ val byteString = factory.decodeHex("112233")
+ assertEquals(0, byteString.lastIndexOf("112233".decodeHex().toByteArray()).toLong())
+ assertEquals(1, byteString.lastIndexOf("2233".decodeHex().toByteArray()).toLong())
+ assertEquals(2, byteString.lastIndexOf("33".decodeHex().toByteArray()).toLong())
+ assertEquals(3, byteString.lastIndexOf("".decodeHex().toByteArray()).toLong())
+ }
+
+ @Test fun equalsTest() {
+ val byteString = factory.decodeHex("000102")
+ assertEquals(byteString, byteString)
+ assertEquals(byteString, "000102".decodeHex())
+ assertNotEquals(byteString, Any())
+ assertNotEquals(byteString, "000201".decodeHex())
+ }
+
+ @Test fun equalsEmptyTest() {
+ assertEquals(factory.decodeHex(""), ByteString.EMPTY)
+ assertEquals(factory.decodeHex(""), ByteString.of())
+ assertEquals(ByteString.EMPTY, factory.decodeHex(""))
+ assertEquals(ByteString.of(), factory.decodeHex(""))
+ }
+
+ private val bronzeHorseman = "На берегу пустынных волн"
+
+ @Test fun utf8() {
+ val byteString = factory.encodeUtf8(bronzeHorseman)
+ assertEquals(byteString.toByteArray().toList(), bronzeHorseman.commonAsUtf8ToByteArray().toList())
+ assertTrue(byteString == ByteString.of(*bronzeHorseman.commonAsUtf8ToByteArray()))
+ assertEquals(
+ byteString,
+ (
+ "d09dd0b020d0b1d0b5d180d0b5d0b3d18320d0bfd183d181" +
+ "d182d18bd0bdd0bdd18bd18520d0b2d0bed0bbd0bd"
+ ).decodeHex()
+ )
+ assertEquals(byteString.utf8(), bronzeHorseman)
+ }
+
+ @Test fun testHashCode() {
+ val byteString = factory.decodeHex("0102")
+ assertEquals(byteString.hashCode().toLong(), byteString.hashCode().toLong())
+ assertEquals(byteString.hashCode().toLong(), "0102".decodeHex().hashCode().toLong())
+ }
+
+ @Test fun toAsciiLowerCaseNoUppercase() {
+ val s = factory.encodeUtf8("a1_+")
+ assertEquals(s, s.toAsciiLowercase())
+ if (factory === ByteStringFactory.BYTE_STRING) {
+ assertSame(s, s.toAsciiLowercase())
+ }
+ }
+
+ @Test fun toAsciiAllUppercase() {
+ assertEquals("ab".encodeUtf8(), factory.encodeUtf8("AB").toAsciiLowercase())
+ }
+
+ @Test fun toAsciiStartsLowercaseEndsUppercase() {
+ assertEquals("abcd".encodeUtf8(), factory.encodeUtf8("abCD").toAsciiLowercase())
+ }
+
+ @Test fun toAsciiStartsUppercaseEndsLowercase() {
+ assertEquals("ABCD".encodeUtf8(), factory.encodeUtf8("ABcd").toAsciiUppercase())
+ }
+
+ @Test fun substring() {
+ val byteString = factory.encodeUtf8("Hello, World!")
+
+ assertEquals(byteString.substring(0), byteString)
+ assertEquals(byteString.substring(0, 5), "Hello".encodeUtf8())
+ assertEquals(byteString.substring(7), "World!".encodeUtf8())
+ assertEquals(byteString.substring(6, 6), "".encodeUtf8())
+ }
+
+ @Test fun substringWithInvalidBounds() {
+ val byteString = factory.encodeUtf8("Hello, World!")
+
+ assertFailsWith<IllegalArgumentException> {
+ byteString.substring(-1)
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ byteString.substring(0, 14)
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ byteString.substring(8, 7)
+ }
+ }
+
+ @Test fun encodeBase64() {
+ assertEquals("", factory.encodeUtf8("").base64())
+ assertEquals("AA==", factory.encodeUtf8("\u0000").base64())
+ assertEquals("AAA=", factory.encodeUtf8("\u0000\u0000").base64())
+ assertEquals("AAAA", factory.encodeUtf8("\u0000\u0000\u0000").base64())
+ assertEquals(
+ "SG93IG1hbnkgbGluZXMgb2YgY29kZSBhcmUgdGhlcmU/ICdib3V0IDIgbWlsbGlvbi4=",
+ factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64()
+ )
+ }
+
+ @Test fun encodeBase64Url() {
+ assertEquals("", factory.encodeUtf8("").base64Url())
+ assertEquals("AA==", factory.encodeUtf8("\u0000").base64Url())
+ assertEquals("AAA=", factory.encodeUtf8("\u0000\u0000").base64Url())
+ assertEquals("AAAA", factory.encodeUtf8("\u0000\u0000\u0000").base64Url())
+ assertEquals(
+ "SG93IG1hbnkgbGluZXMgb2YgY29kZSBhcmUgdGhlcmU_ICdib3V0IDIgbWlsbGlvbi4=",
+ factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64Url()
+ )
+ }
+
+ @Test fun ignoreUnnecessaryPadding() {
+ assertEquals("", "====".decodeBase64()!!.utf8())
+ assertEquals("\u0000\u0000\u0000", "AAAA====".decodeBase64()!!.utf8())
+ }
+
+ @Test fun decodeBase64() {
+ assertEquals("", "".decodeBase64()!!.utf8())
+ assertEquals(null, "/===".decodeBase64()) // Can't do anything with 6 bits!
+ assertEquals("ff".decodeHex(), "//==".decodeBase64())
+ assertEquals("ff".decodeHex(), "__==".decodeBase64())
+ assertEquals("ffff".decodeHex(), "///=".decodeBase64())
+ assertEquals("ffff".decodeHex(), "___=".decodeBase64())
+ assertEquals("ffffff".decodeHex(), "////".decodeBase64())
+ assertEquals("ffffff".decodeHex(), "____".decodeBase64())
+ assertEquals("ffffffffffff".decodeHex(), "////////".decodeBase64())
+ assertEquals("ffffffffffff".decodeHex(), "________".decodeBase64())
+ assertEquals(
+ "What's to be scared about? It's just a little hiccup in the power...",
+ (
+ "V2hhdCdzIHRvIGJlIHNjYXJlZCBhYm91dD8gSXQncyBqdXN0IGEgbGl0dGxlIGhpY2" +
+ "N1cCBpbiB0aGUgcG93ZXIuLi4="
+ ).decodeBase64()!!.utf8()
+ )
+ // Uses two encoding styles. Malformed, but supported as a side-effect.
+ assertEquals("ffffff".decodeHex(), "__//".decodeBase64())
+ }
+
+ @Test fun decodeBase64WithWhitespace() {
+ assertEquals("\u0000\u0000\u0000", " AA AA ".decodeBase64()!!.utf8())
+ assertEquals("\u0000\u0000\u0000", " AA A\r\nA ".decodeBase64()!!.utf8())
+ assertEquals("\u0000\u0000\u0000", "AA AA".decodeBase64()!!.utf8())
+ assertEquals("\u0000\u0000\u0000", " AA AA ".decodeBase64()!!.utf8())
+ assertEquals("\u0000\u0000\u0000", " AA A\r\nA ".decodeBase64()!!.utf8())
+ assertEquals("\u0000\u0000\u0000", "A AAA".decodeBase64()!!.utf8())
+ assertEquals("", " ".decodeBase64()!!.utf8())
+ }
+
+ @Test fun encodeHex() {
+ assertEquals("000102", ByteString.of(0x0, 0x1, 0x2).hex())
+ }
+
+ @Test fun decodeHex() {
+ val actual = "CAFEBABE".decodeHex()
+ val expected = ByteString.of(-54, -2, -70, -66)
+ assertEquals(expected, actual)
+ }
+
+ @Test fun decodeHexOddNumberOfChars() {
+ assertFailsWith<IllegalArgumentException> {
+ "aaa".decodeHex()
+ }
+ }
+
+ @Test fun decodeHexInvalidChar() {
+ assertFailsWith<IllegalArgumentException> {
+ "a\u0000".decodeHex()
+ }
+ }
+
+ @Test fun toStringOnEmpty() {
+ assertEquals("[size=0]", factory.decodeHex("").toString())
+ }
+
+ @Test fun toStringOnShortText() {
+ assertEquals(
+ "[text=Tyrannosaur]",
+ factory.encodeUtf8("Tyrannosaur").toString()
+ )
+ assertEquals(
+ "[text=təˈranəˌsôr]",
+ factory.decodeHex("74c999cb8872616ec999cb8c73c3b472").toString()
+ )
+ }
+
+ @Test fun toStringOnLongTextIsTruncated() {
+ val raw = (
+ "Um, I'll tell you the problem with the scientific power that you're using here, " +
+ "it didn't require any discipline to attain it. You read what others had done and you " +
+ "took the next step. You didn't earn the knowledge for yourselves, so you don't take any " +
+ "responsibility for it. You stood on the shoulders of geniuses to accomplish something " +
+ "as fast as you could, and before you even knew what you had, you patented it, and " +
+ "packaged it, and slapped it on a plastic lunchbox, and now you're selling it, you wanna " +
+ "sell it."
+ )
+ assertEquals(
+ "[size=517 text=Um, I'll tell you the problem with the scientific power that " +
+ "you…]",
+ factory.encodeUtf8(raw).toString()
+ )
+ val war = (
+ "Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 𝛄𝓸𝘂'𝒓𝗲 υ𝖘𝓲𝗇ɡ 𝕙𝚎𝑟e, " +
+ "𝛊𝓽 ⅆ𝕚𝐝𝝿'𝗍 𝔯𝙚𝙦ᴜ𝜾𝒓𝘦 𝔞𝘯𝐲 ԁ𝜄𝑠𝚌ι𝘱lι𝒏e 𝑡𝜎 𝕒𝚝𝖙𝓪і𝞹 𝔦𝚝. 𝒀ο𝗎 𝔯𝑒⍺𝖉 w𝐡𝝰𝔱 𝞂𝞽һ𝓮𝓇ƽ հ𝖺𝖉 ⅾ𝛐𝝅ⅇ 𝝰πԁ 𝔂ᴑᴜ 𝓉ﮨ၀𝚔 " +
+ "т𝒽𝑒 𝗇𝕖ⅹ𝚝 𝔰𝒕е𝓅. 𝘠ⲟ𝖚 𝖉ⅰԁ𝝕'τ 𝙚𝚊r𝞹 𝘵Ꮒ𝖾 𝝒𝐧هwl𝑒𝖉ƍ𝙚 𝓯૦r 𝔂𝞼𝒖𝕣𝑠𝕖l𝙫𝖊𝓼, 𐑈о y𝘰𝒖 ⅆە𝗇't 𝜏α𝒌𝕖 𝛂𝟉ℽ " +
+ "𝐫ⅇ𝗌ⲣ๐ϖ𝖘ꙇᖯ𝓲l𝓲𝒕𝘆 𝐟𝞼𝘳 𝚤𝑡. 𝛶𝛔𝔲 s𝕥σσ𝐝 ﮩ𝕟 𝒕𝗁𝔢 𝘴𝐡𝜎ᴜlⅾ𝓮𝔯𝚜 𝛐𝙛 ᶃ𝚎ᴨᎥս𝚜𝘦𝓈 𝓽𝞸 a𝒄𝚌𝞸mρl𝛊ꜱ𝐡 𝓈𝚘m𝚎𝞃𝔥⍳𝞹𝔤 𝐚𝗌 𝖋a𝐬𝒕 " +
+ "αs γ𝛐𝕦 𝔠ﻫ𝛖lԁ, 𝚊π𝑑 Ь𝑒𝙛૦𝓇𝘦 𝓎٥𝖚 ⅇvℯ𝝅 𝜅ո𝒆w w𝗵𝒂𝘁 ᶌ੦𝗎 h𝐚𝗱, 𝜸ﮨ𝒖 𝓹𝝰𝔱𝖾𝗇𝓽𝔢ⅆ і𝕥, 𝚊𝜛𝓭 𝓹𝖺ⅽϰ𝘢ℊеᏧ 𝑖𝞃, " +
+ "𝐚𝛑ꓒ 𝙨l𝔞р𝘱𝔢𝓭 ɩ𝗍 ہ𝛑 𝕒 pl𝛂ѕᴛ𝗂𝐜 l𝞄ℼ𝔠𝒽𝑏ﮪ⨯, 𝔞ϖ𝒹 n𝛔w 𝛾𝐨𝞄'𝗿𝔢 ꜱ℮ll𝙞nɡ ɩ𝘁, 𝙮𝕠𝛖 w𝑎ℼ𝚗𝛂 𝕤𝓮ll 𝙞𝓉."
+ )
+ assertEquals(
+ "[size=1496 text=Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 " +
+ "𝛄𝓸𝘂…]",
+ factory.encodeUtf8(war).toString()
+ )
+ }
+
+ @Test fun toStringOnTextWithNewlines() {
+ // Instead of emitting a literal newline in the toString(), these are escaped as "\n".
+ assertEquals(
+ "[text=a\\r\\nb\\nc\\rd\\\\e]",
+ factory.encodeUtf8("a\r\nb\nc\rd\\e").toString()
+ )
+ }
+
+ @Test fun toStringOnData() {
+ val byteString = factory.decodeHex(
+ "" +
+ "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" +
+ "4bf0b54023c29b624de9ef9c2f931efc580f9afb"
+ )
+ assertEquals(
+ "[hex=" +
+ "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" +
+ "4bf0b54023c29b624de9ef9c2f931efc580f9afb]",
+ byteString.toString()
+ )
+ }
+
+ @Test fun toStringOnLongDataIsTruncated() {
+ val byteString = factory.decodeHex(
+ "" +
+ "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" +
+ "4bf0b54023c29b624de9ef9c2f931efc580f9afba1"
+ )
+ assertEquals(
+ "[size=65 hex=" +
+ "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55" +
+ "4bf0b54023c29b624de9ef9c2f931efc580f9afb…]",
+ byteString.toString()
+ )
+ }
+
+ @Test fun compareToSingleBytes() {
+ val originalByteStrings = listOf(
+ factory.decodeHex("00"),
+ factory.decodeHex("01"),
+ factory.decodeHex("7e"),
+ factory.decodeHex("7f"),
+ factory.decodeHex("80"),
+ factory.decodeHex("81"),
+ factory.decodeHex("fe"),
+ factory.decodeHex("ff")
+ )
+
+ val sortedByteStrings = originalByteStrings.toMutableList()
+ sortedByteStrings.shuffle(Random(0))
+ assertNotEquals(originalByteStrings, sortedByteStrings)
+
+ sortedByteStrings.sort()
+ assertEquals(originalByteStrings, sortedByteStrings)
+ }
+
+ @Test fun compareToMultipleBytes() {
+ val originalByteStrings = listOf(
+ factory.decodeHex(""),
+ factory.decodeHex("00"),
+ factory.decodeHex("0000"),
+ factory.decodeHex("000000"),
+ factory.decodeHex("00000000"),
+ factory.decodeHex("0000000000"),
+ factory.decodeHex("0000000001"),
+ factory.decodeHex("000001"),
+ factory.decodeHex("00007f"),
+ factory.decodeHex("0000ff"),
+ factory.decodeHex("000100"),
+ factory.decodeHex("000101"),
+ factory.decodeHex("007f00"),
+ factory.decodeHex("00ff00"),
+ factory.decodeHex("010000"),
+ factory.decodeHex("010001"),
+ factory.decodeHex("01007f"),
+ factory.decodeHex("0100ff"),
+ factory.decodeHex("010100"),
+ factory.decodeHex("01010000"),
+ factory.decodeHex("0101000000"),
+ factory.decodeHex("0101000001"),
+ factory.decodeHex("010101"),
+ factory.decodeHex("7f0000"),
+ factory.decodeHex("7f0000ffff"),
+ factory.decodeHex("ffffff")
+ )
+
+ val sortedByteStrings = originalByteStrings.toMutableList()
+ sortedByteStrings.shuffle(Random(0))
+ assertNotEquals(originalByteStrings, sortedByteStrings)
+
+ sortedByteStrings.sort()
+ assertEquals(originalByteStrings, sortedByteStrings)
+ }
+
+ @Test fun testHash() = with(factory.encodeUtf8("Kevin")) {
+ assertEquals("e043899daa0c7add37bc99792b2c045d6abbc6dc", sha1().hex())
+ assertEquals("f1cd318e412b5f7226e5f377a9544ff7", md5().hex())
+ assertEquals("0e4dd66217fc8d2e298b78c8cd9392870dcd065d0ff675d0edff5bcd227837e9", sha256().hex())
+ assertEquals("483676b93c4417198b465083d196ec6a9fab8d004515874b8ff47e041f5f56303cc08179625030b8b5b721c09149a18f0f59e64e7ae099518cea78d3d83167e1", sha512().hex())
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/CommonBufferTest.kt b/okio/src/commonTest/kotlin/okio/CommonBufferTest.kt
new file mode 100644
index 00000000..292e3c5c
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/CommonBufferTest.kt
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.ByteString.Companion.decodeHex
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+/**
+ * Tests solely for the behavior of Buffer's implementation. For generic BufferedSink or
+ * BufferedSource behavior use BufferedSinkTest or BufferedSourceTest, respectively.
+ */
+class CommonBufferTest {
+ @Test fun readAndWriteUtf8() {
+ val buffer = Buffer()
+ buffer.writeUtf8("ab")
+ assertEquals(2, buffer.size)
+ buffer.writeUtf8("cdef")
+ assertEquals(6, buffer.size)
+ assertEquals("abcd", buffer.readUtf8(4))
+ assertEquals(2, buffer.size)
+ assertEquals("ef", buffer.readUtf8(2))
+ assertEquals(0, buffer.size)
+ assertFailsWith<EOFException> {
+ buffer.readUtf8(1)
+ }
+ }
+
+ /** Buffer's toString is the same as ByteString's. */
+ @Test fun bufferToString() {
+ assertEquals("[size=0]", Buffer().toString())
+ assertEquals(
+ "[text=a\\r\\nb\\nc\\rd\\\\e]",
+ Buffer().writeUtf8("a\r\nb\nc\rd\\e").toString()
+ )
+ assertEquals(
+ "[text=Tyrannosaur]",
+ Buffer().writeUtf8("Tyrannosaur").toString()
+ )
+ assertEquals(
+ "[text=təˈranəˌsôr]",
+ Buffer()
+ .write("74c999cb8872616ec999cb8c73c3b472".decodeHex())
+ .toString()
+ )
+ assertEquals(
+ "[hex=0000000000000000000000000000000000000000000000000000000000000000000000000000" +
+ "0000000000000000000000000000000000000000000000000000]",
+ Buffer().write(ByteArray(64)).toString()
+ )
+ }
+
+ @Test fun multipleSegmentBuffers() {
+ val buffer = Buffer()
+ buffer.writeUtf8('a'.repeat(1000))
+ buffer.writeUtf8('b'.repeat(2500))
+ buffer.writeUtf8('c'.repeat(5000))
+ buffer.writeUtf8('d'.repeat(10000))
+ buffer.writeUtf8('e'.repeat(25000))
+ buffer.writeUtf8('f'.repeat(50000))
+
+ assertEquals('a'.repeat(999), buffer.readUtf8(999)) // a...a
+ assertEquals("a" + 'b'.repeat(2500) + "c", buffer.readUtf8(2502)) // ab...bc
+ assertEquals('c'.repeat(4998), buffer.readUtf8(4998)) // c...c
+ assertEquals("c" + 'd'.repeat(10000) + "e", buffer.readUtf8(10002)) // cd...de
+ assertEquals('e'.repeat(24998), buffer.readUtf8(24998)) // e...e
+ assertEquals("e" + 'f'.repeat(50000), buffer.readUtf8(50001)) // ef...f
+ assertEquals(0, buffer.size)
+ }
+
+ @Test fun fillAndDrainPool() {
+ val buffer = Buffer()
+
+ // Take 2 * MAX_SIZE segments. This will drain the pool, even if other tests filled it.
+ buffer.write(ByteArray(SegmentPool.MAX_SIZE))
+ buffer.write(ByteArray(SegmentPool.MAX_SIZE))
+ assertEquals(0, SegmentPool.byteCount)
+
+ // Recycle MAX_SIZE segments. They're all in the pool.
+ buffer.skip(SegmentPool.MAX_SIZE.toLong())
+ assertEquals(SegmentPool.MAX_SIZE, SegmentPool.byteCount)
+
+ // Recycle MAX_SIZE more segments. The pool is full so they get garbage collected.
+ buffer.skip(SegmentPool.MAX_SIZE.toLong())
+ assertEquals(SegmentPool.MAX_SIZE, SegmentPool.byteCount)
+
+ // Take MAX_SIZE segments to drain the pool.
+ buffer.write(ByteArray(SegmentPool.MAX_SIZE))
+ assertEquals(0, SegmentPool.byteCount)
+
+ // Take MAX_SIZE more segments. The pool is drained so these will need to be allocated.
+ buffer.write(ByteArray(SegmentPool.MAX_SIZE))
+ assertEquals(0, SegmentPool.byteCount)
+ }
+
+ @Test fun moveBytesBetweenBuffersShareSegment() {
+ val size = Segment.SIZE / 2 - 1
+ val segmentSizes = moveBytesBetweenBuffers('a'.repeat(size), 'b'.repeat(size))
+ assertEquals(listOf(size * 2), segmentSizes)
+ }
+
+ @Test fun moveBytesBetweenBuffersReassignSegment() {
+ val size = Segment.SIZE / 2 + 1
+ val segmentSizes = moveBytesBetweenBuffers('a'.repeat(size), 'b'.repeat(size))
+ assertEquals(listOf(size, size), segmentSizes)
+ }
+
+ @Test fun moveBytesBetweenBuffersMultipleSegments() {
+ val size = 3 * Segment.SIZE + 1
+ val segmentSizes = moveBytesBetweenBuffers('a'.repeat(size), 'b'.repeat(size))
+ assertEquals(
+ listOf(
+ Segment.SIZE, Segment.SIZE, Segment.SIZE, 1,
+ Segment.SIZE, Segment.SIZE, Segment.SIZE, 1
+ ),
+ segmentSizes
+ )
+ }
+
+ private fun moveBytesBetweenBuffers(vararg contents: String): List<Int> {
+ val expected = StringBuilder()
+ val buffer = Buffer()
+ for (s in contents) {
+ val source = Buffer()
+ source.writeUtf8(s)
+ buffer.writeAll(source)
+ expected.append(s)
+ }
+ val segmentSizes = segmentSizes(buffer)
+ assertEquals(expected.toString(), buffer.readUtf8(expected.length.toLong()))
+ return segmentSizes
+ }
+
+ /** The big part of source's first segment is being moved. */
+ @Test fun writeSplitSourceBufferLeft() {
+ val writeSize = Segment.SIZE / 2 + 1
+
+ val sink = Buffer()
+ sink.writeUtf8('b'.repeat(Segment.SIZE - 10))
+
+ val source = Buffer()
+ source.writeUtf8('a'.repeat(Segment.SIZE * 2))
+ sink.write(source, writeSize.toLong())
+
+ assertEquals(listOf(Segment.SIZE - 10, writeSize), segmentSizes(sink))
+ assertEquals(listOf(Segment.SIZE - writeSize, Segment.SIZE), segmentSizes(source))
+ }
+
+ /** The big part of source's first segment is staying put. */
+ @Test fun writeSplitSourceBufferRight() {
+ val writeSize = Segment.SIZE / 2 - 1
+
+ val sink = Buffer()
+ sink.writeUtf8('b'.repeat(Segment.SIZE - 10))
+
+ val source = Buffer()
+ source.writeUtf8('a'.repeat(Segment.SIZE * 2))
+ sink.write(source, writeSize.toLong())
+
+ assertEquals(listOf(Segment.SIZE - 10, writeSize), segmentSizes(sink))
+ assertEquals(listOf(Segment.SIZE - writeSize, Segment.SIZE), segmentSizes(source))
+ }
+
+ @Test fun writePrefixDoesntSplit() {
+ val sink = Buffer()
+ sink.writeUtf8('b'.repeat(10))
+
+ val source = Buffer()
+ source.writeUtf8('a'.repeat(Segment.SIZE * 2))
+ sink.write(source, 20)
+
+ assertEquals(listOf(30), segmentSizes(sink))
+ assertEquals(listOf(Segment.SIZE - 20, Segment.SIZE), segmentSizes(source))
+ assertEquals(30, sink.size)
+ assertEquals((Segment.SIZE * 2 - 20).toLong(), source.size)
+ }
+
+ @Test fun writePrefixDoesntSplitButRequiresCompact() {
+ val sink = Buffer()
+ sink.writeUtf8('b'.repeat(Segment.SIZE - 10)) // limit = size - 10
+ sink.readUtf8((Segment.SIZE - 20).toLong()) // pos = size = 20
+
+ val source = Buffer()
+ source.writeUtf8('a'.repeat(Segment.SIZE * 2))
+ sink.write(source, 20)
+
+ assertEquals(listOf(30), segmentSizes(sink))
+ assertEquals(listOf(Segment.SIZE - 20, Segment.SIZE), segmentSizes(source))
+ assertEquals(30, sink.size)
+ assertEquals((Segment.SIZE * 2 - 20).toLong(), source.size)
+ }
+
+ @Test fun moveAllRequestedBytesWithRead() {
+ val sink = Buffer()
+ sink.writeUtf8('a'.repeat(10))
+
+ val source = Buffer()
+ source.writeUtf8('b'.repeat(15))
+
+ assertEquals(10, source.read(sink, 10))
+ assertEquals(20, sink.size)
+ assertEquals(5, source.size)
+ assertEquals('a'.repeat(10) + 'b'.repeat(10), sink.readUtf8(20))
+ }
+
+ @Test fun moveFewerThanRequestedBytesWithRead() {
+ val sink = Buffer()
+ sink.writeUtf8('a'.repeat(10))
+
+ val source = Buffer()
+ source.writeUtf8('b'.repeat(20))
+
+ assertEquals(20, source.read(sink, 25))
+ assertEquals(30, sink.size)
+ assertEquals(0, source.size)
+ assertEquals('a'.repeat(10) + 'b'.repeat(20), sink.readUtf8(30))
+ }
+
+ @Test fun indexOfWithOffset() {
+ val buffer = Buffer()
+ val halfSegment = Segment.SIZE / 2
+ buffer.writeUtf8('a'.repeat(halfSegment))
+ buffer.writeUtf8('b'.repeat(halfSegment))
+ buffer.writeUtf8('c'.repeat(halfSegment))
+ buffer.writeUtf8('d'.repeat(halfSegment))
+ assertEquals(0, buffer.indexOf('a'.toByte(), 0))
+ assertEquals((halfSegment - 1).toLong(), buffer.indexOf('a'.toByte(), (halfSegment - 1).toLong()))
+ assertEquals(halfSegment.toLong(), buffer.indexOf('b'.toByte(), (halfSegment - 1).toLong()))
+ assertEquals((halfSegment * 2).toLong(), buffer.indexOf('c'.toByte(), (halfSegment - 1).toLong()))
+ assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.toByte(), (halfSegment - 1).toLong()))
+ assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.toByte(), (halfSegment * 2).toLong()))
+ assertEquals((halfSegment * 3).toLong(), buffer.indexOf('d'.toByte(), (halfSegment * 3).toLong()))
+ assertEquals((halfSegment * 4 - 1).toLong(), buffer.indexOf('d'.toByte(), (halfSegment * 4 - 1).toLong()))
+ }
+
+ @Test fun byteAt() {
+ val buffer = Buffer()
+ buffer.writeUtf8("a")
+ buffer.writeUtf8('b'.repeat(Segment.SIZE))
+ buffer.writeUtf8("c")
+ assertEquals('a'.toLong(), buffer[0].toLong())
+ assertEquals('a'.toLong(), buffer[0].toLong()) // getByte doesn't mutate!
+ assertEquals('c'.toLong(), buffer[buffer.size - 1].toLong())
+ assertEquals('b'.toLong(), buffer[buffer.size - 2].toLong())
+ assertEquals('b'.toLong(), buffer[buffer.size - 3].toLong())
+ }
+
+ @Test fun getByteOfEmptyBuffer() {
+ val buffer = Buffer()
+ assertFailsWith<IndexOutOfBoundsException> {
+ buffer[0]
+ }
+ }
+
+ @Test
+ fun writePrefixToEmptyBuffer() {
+ val sink = Buffer()
+ val source = Buffer()
+ source.writeUtf8("abcd")
+ sink.write(source, 2)
+ assertEquals("ab", sink.readUtf8(2))
+ }
+
+ @Suppress("ReplaceAssertBooleanWithAssertEquality")
+ @Test fun equalsAndHashCodeEmpty() {
+ val a = Buffer()
+ val b = Buffer()
+ assertTrue(a == b)
+ assertTrue(a.hashCode() == b.hashCode())
+ }
+
+ @Suppress("ReplaceAssertBooleanWithAssertEquality")
+ @Test fun equalsAndHashCode() {
+ val a = Buffer().writeUtf8("dog")
+ val b = Buffer().writeUtf8("hotdog")
+ assertFalse(a == b)
+ assertFalse(a.hashCode() == b.hashCode())
+
+ b.readUtf8(3) // Leaves b containing 'dog'.
+ assertTrue(a == b)
+ assertTrue(a.hashCode() == b.hashCode())
+ }
+
+ @Suppress("ReplaceAssertBooleanWithAssertEquality")
+ @Test fun equalsAndHashCodeSpanningSegments() {
+ val data = ByteArray(1024 * 1024)
+ val dice = Random(0)
+ dice.nextBytes(data)
+
+ val a = bufferWithRandomSegmentLayout(dice, data)
+ val b = bufferWithRandomSegmentLayout(dice, data)
+ assertTrue(a == b)
+ assertTrue(a.hashCode() == b.hashCode())
+
+ data[data.size / 2]++ // Change a single byte.
+ val c = bufferWithRandomSegmentLayout(dice, data)
+ assertFalse(a == c)
+ assertFalse(a.hashCode() == c.hashCode())
+ }
+
+ /**
+ * When writing data that's already buffered, there's no reason to page the
+ * data by segment.
+ */
+ @Test fun readAllWritesAllSegmentsAtOnce() {
+ val write1 = Buffer().writeUtf8(
+ 'a'.repeat(Segment.SIZE) +
+ 'b'.repeat(Segment.SIZE) +
+ 'c'.repeat(Segment.SIZE)
+ )
+
+ val source = Buffer().writeUtf8(
+ 'a'.repeat(Segment.SIZE) +
+ 'b'.repeat(Segment.SIZE) +
+ 'c'.repeat(Segment.SIZE)
+ )
+
+ val mockSink = MockSink()
+
+ assertEquals((Segment.SIZE * 3).toLong(), source.readAll(mockSink))
+ assertEquals(0, source.size)
+ mockSink.assertLog("write($write1, ${write1.size})")
+ }
+
+ @Test fun writeAllMultipleSegments() {
+ val source = Buffer().writeUtf8('a'.repeat(Segment.SIZE * 3))
+ val sink = Buffer()
+
+ assertEquals((Segment.SIZE * 3).toLong(), sink.writeAll(source))
+ assertEquals(0, source.size)
+ assertEquals('a'.repeat(Segment.SIZE * 3), sink.readUtf8())
+ }
+
+ @Test fun copyTo() {
+ val source = Buffer()
+ source.writeUtf8("party")
+
+ val target = Buffer()
+ source.copyTo(target, 1, 3)
+
+ assertEquals("art", target.readUtf8())
+ assertEquals("party", source.readUtf8())
+ }
+
+ @Test fun copyToOnSegmentBoundary() {
+ val `as` = 'a'.repeat(Segment.SIZE)
+ val bs = 'b'.repeat(Segment.SIZE)
+ val cs = 'c'.repeat(Segment.SIZE)
+ val ds = 'd'.repeat(Segment.SIZE)
+
+ val source = Buffer()
+ source.writeUtf8(`as`)
+ source.writeUtf8(bs)
+ source.writeUtf8(cs)
+
+ val target = Buffer()
+ target.writeUtf8(ds)
+
+ source.copyTo(target, `as`.length.toLong(), (bs.length + cs.length).toLong())
+ assertEquals(ds + bs + cs, target.readUtf8())
+ }
+
+ @Test fun copyToOffSegmentBoundary() {
+ val `as` = 'a'.repeat(Segment.SIZE - 1)
+ val bs = 'b'.repeat(Segment.SIZE + 2)
+ val cs = 'c'.repeat(Segment.SIZE - 4)
+ val ds = 'd'.repeat(Segment.SIZE + 8)
+
+ val source = Buffer()
+ source.writeUtf8(`as`)
+ source.writeUtf8(bs)
+ source.writeUtf8(cs)
+
+ val target = Buffer()
+ target.writeUtf8(ds)
+
+ source.copyTo(target, `as`.length.toLong(), (bs.length + cs.length).toLong())
+ assertEquals(ds + bs + cs, target.readUtf8())
+ }
+
+ @Test fun copyToSourceAndTargetCanBeTheSame() {
+ val `as` = 'a'.repeat(Segment.SIZE)
+ val bs = 'b'.repeat(Segment.SIZE)
+
+ val source = Buffer()
+ source.writeUtf8(`as`)
+ source.writeUtf8(bs)
+
+ source.copyTo(source, 0, source.size)
+ assertEquals(`as` + bs + `as` + bs, source.readUtf8())
+ }
+
+ @Test fun copyToEmptySource() {
+ val source = Buffer()
+ val target = Buffer().writeUtf8("aaa")
+ source.copyTo(target, 0L, 0L)
+ assertEquals("", source.readUtf8())
+ assertEquals("aaa", target.readUtf8())
+ }
+
+ @Test fun copyToEmptyTarget() {
+ val source = Buffer().writeUtf8("aaa")
+ val target = Buffer()
+ source.copyTo(target, 0L, 3L)
+ assertEquals("aaa", source.readUtf8())
+ assertEquals("aaa", target.readUtf8())
+ }
+
+ @Test fun snapshotReportsAccurateSize() {
+ val buf = Buffer().write(byteArrayOf(0, 1, 2, 3))
+ assertEquals(1, buf.snapshot(1).size)
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/CommonOkioKotlinTest.kt b/okio/src/commonTest/kotlin/okio/CommonOkioKotlinTest.kt
new file mode 100644
index 00000000..131dffb7
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/CommonOkioKotlinTest.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class CommonOkioKotlinTest {
+ @Test fun sourceBuffer() {
+ val source = Buffer().writeUtf8("a")
+ val buffered = (source as Source).buffer()
+ assertEquals(buffered.readUtf8(), "a")
+ assertEquals(source.size, 0L)
+ }
+
+ @Test fun sinkBuffer() {
+ val sink = Buffer()
+ val buffered = (sink as Sink).buffer()
+ buffered.writeUtf8("a")
+ assertEquals(sink.size, 0L)
+ buffered.flush()
+ assertEquals(sink.size, 1L)
+ }
+
+ @Test fun blackhole() {
+ blackholeSink().write(Buffer().writeUtf8("a"), 1L)
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/CommonOptionsTest.kt b/okio/src/commonTest/kotlin/okio/CommonOptionsTest.kt
new file mode 100644
index 00000000..bb45321d
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/CommonOptionsTest.kt
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.ByteString.Companion.encodeUtf8
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.fail
+
+class CommonOptionsTest {
+ /** Confirm that options prefers the first-listed option, not the longest or shortest one. */
+ @Test fun optionOrderTakesPrecedence() {
+ assertSelect("abcdefg", 0, "abc", "abcdef")
+ assertSelect("abcdefg", 0, "abcdef", "abc")
+ }
+
+ @Test fun simpleOptionsTrie() {
+ assertEquals(
+ utf8Options("hotdog", "hoth", "hot").trieString(),
+ """
+ |hot
+ | -> 2
+ | d
+ | og -> 0
+ | h -> 1
+ |""".trimMargin()
+ )
+ }
+
+ @Test fun realisticOptionsTrie() {
+ // These are the fields of OkHttpClient in 3.10.
+ val options = utf8Options(
+ "dispatcher",
+ "proxy",
+ "protocols",
+ "connectionSpecs",
+ "interceptors",
+ "networkInterceptors",
+ "eventListenerFactory",
+ "proxySelector", // No index 7 in the trie because 'proxy' is a prefix!
+ "cookieJar",
+ "cache",
+ "internalCache",
+ "socketFactory",
+ "sslSocketFactory",
+ "certificateChainCleaner",
+ "hostnameVerifier",
+ "certificatePinner",
+ "proxyAuthenticator", // No index 16 in the trie because 'proxy' is a prefix!
+ "authenticator",
+ "connectionPool",
+ "dns",
+ "followSslRedirects",
+ "followRedirects",
+ "retryOnConnectionFailure",
+ "connectTimeout",
+ "readTimeout",
+ "writeTimeout",
+ "pingInterval"
+ )
+ assertEquals(
+ options.trieString(),
+ """
+ |a
+ | uthenticator -> 17
+ |c
+ | a
+ | che -> 9
+ | e
+ | rtificate
+ | C
+ | hainCleaner -> 13
+ | P
+ | inner -> 15
+ | o
+ | n
+ | nect
+ | T
+ | imeout -> 23
+ | i
+ | on
+ | P
+ | ool -> 18
+ | S
+ | pecs -> 3
+ | o
+ | kieJar -> 8
+ |d
+ | i
+ | spatcher -> 0
+ | n
+ | s -> 19
+ |e
+ | ventListenerFactory -> 6
+ |f
+ | ollow
+ | R
+ | edirects -> 21
+ | S
+ | slRedirects -> 20
+ |h
+ | ostnameVerifier -> 14
+ |i
+ | nter
+ | c
+ | eptors -> 4
+ | n
+ | alCache -> 10
+ |n
+ | etworkInterceptors -> 5
+ |p
+ | i
+ | ngInterval -> 26
+ | r
+ | o
+ | t
+ | ocols -> 2
+ | x
+ | y -> 1
+ |r
+ | e
+ | a
+ | dTimeout -> 24
+ | t
+ | ryOnConnectionFailure -> 22
+ |s
+ | o
+ | cketFactory -> 11
+ | s
+ | lSocketFactory -> 12
+ |w
+ | riteTimeout -> 25
+ |""".trimMargin()
+ )
+ assertSelect("", -1, options)
+ assertSelect("a", -1, options)
+ assertSelect("eventListenerFactor", -1, options)
+ assertSelect("dnst", 19, options)
+ assertSelect("proxyproxy", 1, options)
+ assertSelect("prox", -1, options)
+
+ assertSelect("dispatcher", 0, options)
+ assertSelect("proxy", 1, options)
+ assertSelect("protocols", 2, options)
+ assertSelect("connectionSpecs", 3, options)
+ assertSelect("interceptors", 4, options)
+ assertSelect("networkInterceptors", 5, options)
+ assertSelect("eventListenerFactory", 6, options)
+ assertSelect("proxySelector", 1, options) // 'proxy' is a prefix.
+ assertSelect("cookieJar", 8, options)
+ assertSelect("cache", 9, options)
+ assertSelect("internalCache", 10, options)
+ assertSelect("socketFactory", 11, options)
+ assertSelect("sslSocketFactory", 12, options)
+ assertSelect("certificateChainCleaner", 13, options)
+ assertSelect("hostnameVerifier", 14, options)
+ assertSelect("certificatePinner", 15, options)
+ assertSelect("proxyAuthenticator", 1, options) // 'proxy' is a prefix.
+ assertSelect("authenticator", 17, options)
+ assertSelect("connectionPool", 18, options)
+ assertSelect("dns", 19, options)
+ assertSelect("followSslRedirects", 20, options)
+ assertSelect("followRedirects", 21, options)
+ assertSelect("retryOnConnectionFailure", 22, options)
+ assertSelect("connectTimeout", 23, options)
+ assertSelect("readTimeout", 24, options)
+ assertSelect("writeTimeout", 25, options)
+ assertSelect("pingInterval", 26, options)
+ }
+
+ @Test fun emptyOptions() {
+ val options = utf8Options()
+ assertSelect("", -1, options)
+ assertSelect("a", -1, options)
+ assertSelect("abc", -1, options)
+ }
+
+ @Test fun emptyStringInOptionsTrie() {
+ assertFailsWith<IllegalArgumentException> {
+ utf8Options("")
+ }
+ assertFailsWith<IllegalArgumentException> {
+ utf8Options("abc", "")
+ }
+ }
+
+ @Test fun multipleIdenticalValues() {
+ try {
+ utf8Options("abc", "abc")
+ fail()
+ } catch (expected: IllegalArgumentException) {
+ assertEquals(expected.message, "duplicate option: [text=abc]")
+ }
+ }
+
+ @Test fun prefixesAreStripped() {
+ val options = utf8Options("abcA", "abc", "abcB")
+ assertEquals(
+ options.trieString(),
+ """
+ |abc
+ | -> 1
+ | A -> 0
+ |""".trimMargin()
+ )
+ assertSelect("abc", 1, options)
+ assertSelect("abcA", 0, options)
+ assertSelect("abcB", 1, options)
+ assertSelect("abcC", 1, options)
+ assertSelect("ab", -1, options)
+ }
+
+ @Test fun multiplePrefixesAreStripped() {
+ assertEquals(
+ utf8Options("a", "ab", "abc", "abcd", "abcde").trieString(),
+ """
+ |a -> 0
+ |""".trimMargin()
+ )
+ assertEquals(
+ utf8Options("abc", "a", "ab", "abe", "abcd", "abcf").trieString(),
+ """
+ |a
+ | -> 1
+ | bc -> 0
+ |""".trimMargin()
+ )
+ assertEquals(
+ utf8Options("abc", "ab", "a").trieString(),
+ """
+ |a
+ | -> 2
+ | b
+ | -> 1
+ | c -> 0
+ |""".trimMargin()
+ )
+ assertEquals(
+ utf8Options("abcd", "abce", "abc", "abcf", "abcg").trieString(),
+ """
+ |abc
+ | -> 2
+ | d -> 0
+ | e -> 1
+ |""".trimMargin()
+ )
+ }
+
+ @Test fun scan() {
+ val options = utf8Options("abc")
+ assertSelect("abcde", 0, options)
+ }
+
+ @Test fun scanReturnsPrefix() {
+ val options = utf8Options("abcdefg", "ab")
+ assertSelect("ab", 1, options)
+ assertSelect("abcd", 1, options)
+ assertSelect("abcdefg", 0, options)
+ assertSelect("abcdefghi", 0, options)
+ assertSelect("abcdhi", 1, options)
+ }
+
+ @Test fun select() {
+ val options = utf8Options("a", "b", "c")
+ assertSelect("a", 0, options)
+ assertSelect("b", 1, options)
+ assertSelect("c", 2, options)
+ assertSelect("d", -1, options)
+ assertSelect("aa", 0, options)
+ assertSelect("bb", 1, options)
+ assertSelect("cc", 2, options)
+ assertSelect("dd", -1, options)
+ }
+
+ @Test fun selectSelect() {
+ val options = utf8Options("aa", "ab", "ba", "bb")
+ assertSelect("a", -1, options)
+ assertSelect("b", -1, options)
+ assertSelect("c", -1, options)
+ assertSelect("aa", 0, options)
+ assertSelect("ab", 1, options)
+ assertSelect("ac", -1, options)
+ assertSelect("ba", 2, options)
+ assertSelect("bb", 3, options)
+ assertSelect("bc", -1, options)
+ assertSelect("ca", -1, options)
+ assertSelect("cb", -1, options)
+ assertSelect("cc", -1, options)
+ }
+
+ @Test fun selectScan() {
+ val options = utf8Options("abcd", "defg")
+ assertSelect("a", -1, options)
+ assertSelect("d", -1, options)
+ assertSelect("h", -1, options)
+ assertSelect("ab", -1, options)
+ assertSelect("ae", -1, options)
+ assertSelect("de", -1, options)
+ assertSelect("db", -1, options)
+ assertSelect("hi", -1, options)
+ assertSelect("abcd", 0, options)
+ assertSelect("aefg", -1, options)
+ assertSelect("defg", 1, options)
+ assertSelect("dbcd", -1, options)
+ assertSelect("hijk", -1, options)
+ assertSelect("abcdh", 0, options)
+ assertSelect("defgh", 1, options)
+ assertSelect("hijkl", -1, options)
+ }
+
+ @Test fun scanSelect() {
+ val options = utf8Options("abcd", "abce")
+ assertSelect("a", -1, options)
+ assertSelect("f", -1, options)
+ assertSelect("abc", -1, options)
+ assertSelect("abf", -1, options)
+ assertSelect("abcd", 0, options)
+ assertSelect("abce", 1, options)
+ assertSelect("abcf", -1, options)
+ assertSelect("abcdf", 0, options)
+ assertSelect("abcef", 1, options)
+ }
+
+ @Test fun scanSpansSegments() {
+ val options = utf8Options("abcd")
+ assertSelect(bufferWithSegments("a", "bcd"), 0, options)
+ assertSelect(bufferWithSegments("a", "bcde"), 0, options)
+ assertSelect(bufferWithSegments("ab", "cd"), 0, options)
+ assertSelect(bufferWithSegments("ab", "cde"), 0, options)
+ assertSelect(bufferWithSegments("abc", "d"), 0, options)
+ assertSelect(bufferWithSegments("abc", "de"), 0, options)
+ assertSelect(bufferWithSegments("abcd", "e"), 0, options)
+ assertSelect(bufferWithSegments("a", "bce"), -1, options)
+ assertSelect(bufferWithSegments("a", "bce"), -1, options)
+ assertSelect(bufferWithSegments("ab", "ce"), -1, options)
+ assertSelect(bufferWithSegments("ab", "ce"), -1, options)
+ assertSelect(bufferWithSegments("abc", "e"), -1, options)
+ assertSelect(bufferWithSegments("abc", "ef"), -1, options)
+ assertSelect(bufferWithSegments("abce", "f"), -1, options)
+ }
+
+ @Test fun selectSpansSegments() {
+ val options = utf8Options("aa", "ab", "ba", "bb")
+ assertSelect(bufferWithSegments("a", "a"), 0, options)
+ assertSelect(bufferWithSegments("a", "b"), 1, options)
+ assertSelect(bufferWithSegments("a", "c"), -1, options)
+ assertSelect(bufferWithSegments("b", "a"), 2, options)
+ assertSelect(bufferWithSegments("b", "b"), 3, options)
+ assertSelect(bufferWithSegments("b", "c"), -1, options)
+ assertSelect(bufferWithSegments("c", "a"), -1, options)
+ assertSelect(bufferWithSegments("c", "b"), -1, options)
+ assertSelect(bufferWithSegments("c", "c"), -1, options)
+ assertSelect(bufferWithSegments("a", "ad"), 0, options)
+ assertSelect(bufferWithSegments("a", "bd"), 1, options)
+ assertSelect(bufferWithSegments("a", "cd"), -1, options)
+ assertSelect(bufferWithSegments("b", "ad"), 2, options)
+ assertSelect(bufferWithSegments("b", "bd"), 3, options)
+ assertSelect(bufferWithSegments("b", "cd"), -1, options)
+ assertSelect(bufferWithSegments("c", "ad"), -1, options)
+ assertSelect(bufferWithSegments("c", "bd"), -1, options)
+ assertSelect(bufferWithSegments("c", "cd"), -1, options)
+ }
+
+ private fun utf8Options(vararg options: String): Options {
+ return Options.of(*options.map { it.encodeUtf8() }.toTypedArray())
+ }
+
+ private fun assertSelect(data: String, expected: Int, options: Options) {
+ assertSelect(Buffer().writeUtf8(data), expected, options)
+ }
+
+ private fun assertSelect(data: String, expected: Int, vararg options: String) {
+ assertSelect(data, expected, utf8Options(*options))
+ }
+
+ private fun assertSelect(data: Buffer, expected: Int, options: Options) {
+ val initialSize = data.size
+ val actual = data.select(options)
+
+ assertEquals(actual, expected)
+ if (expected == -1) {
+ assertEquals(data.size, initialSize)
+ } else {
+ assertEquals(data.size + options[expected].size, initialSize)
+ }
+ }
+
+ private fun Options.trieString(): String {
+ val result = StringBuilder()
+ printTrieNode(result, 0)
+ return result.toString()
+ }
+
+ private fun Options.printTrieNode(out: StringBuilder, offset: Int = 0, indent: String = "") {
+ if (trie[offset + 1] != -1) {
+ // Print the prefix.
+ out.append("$indent-> ${trie[offset + 1]}\n")
+ }
+
+ if (trie[offset] > 0) {
+ // Print the select.
+ val selectChoiceCount = trie[offset]
+ for (i in 0 until selectChoiceCount) {
+ out.append("$indent${trie[offset + 2 + i].toChar()}")
+ printTrieResult(out, trie[offset + 2 + selectChoiceCount + i], "$indent ")
+ }
+ } else {
+ // Print the scan.
+ val scanByteCount = -1 * trie[offset]
+ out.append(indent)
+ for (i in 0 until scanByteCount) {
+ out.append(trie[offset + 2 + i].toChar())
+ }
+ printTrieResult(out, trie[offset + 2 + scanByteCount], "$indent${" ".repeat(scanByteCount)}")
+ }
+ }
+
+ private fun Options.printTrieResult(out: StringBuilder, result: Int, indent: String) {
+ if (result >= 0) {
+ out.append(" -> $result\n")
+ } else {
+ out.append("\n")
+ printTrieNode(out, -1 * result, indent)
+ }
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/CommonRealBufferedSinkTest.kt b/okio/src/commonTest/kotlin/okio/CommonRealBufferedSinkTest.kt
new file mode 100644
index 00000000..abbbae8b
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/CommonRealBufferedSinkTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.fail
+
+/**
+ * Tests solely for the behavior of RealBufferedSink's implementation. For generic
+ * BufferedSink behavior use BufferedSinkTest.
+ */
+class CommonRealBufferedSinkTest {
+ @Test fun bufferedSinkEmitsTailWhenItIsComplete() {
+ val sink = Buffer()
+ val bufferedSink = (sink as Sink).buffer()
+ bufferedSink.writeUtf8("a".repeat(Segment.SIZE - 1))
+ assertEquals(0, sink.size)
+ bufferedSink.writeByte(0)
+ assertEquals(Segment.SIZE.toLong(), sink.size)
+ assertEquals(0, bufferedSink.buffer.size)
+ }
+
+ @Test fun bufferedSinkEmitMultipleSegments() {
+ val sink = Buffer()
+ val bufferedSink = (sink as Sink).buffer()
+ bufferedSink.writeUtf8("a".repeat(Segment.SIZE * 4 - 1))
+ assertEquals(Segment.SIZE.toLong() * 3L, sink.size)
+ assertEquals(Segment.SIZE.toLong() - 1L, bufferedSink.buffer.size)
+ }
+
+ @Test fun bufferedSinkFlush() {
+ val sink = Buffer()
+ val bufferedSink = (sink as Sink).buffer()
+ bufferedSink.writeByte('a'.toInt())
+ assertEquals(0, sink.size)
+ bufferedSink.flush()
+ assertEquals(0, bufferedSink.buffer.size)
+ assertEquals(1, sink.size)
+ }
+
+ @Test fun bytesEmittedToSinkWithFlush() {
+ val sink = Buffer()
+ val bufferedSink = (sink as Sink).buffer()
+ bufferedSink.writeUtf8("abc")
+ bufferedSink.flush()
+ assertEquals(3, sink.size)
+ }
+
+ @Test fun bytesNotEmittedToSinkWithoutFlush() {
+ val sink = Buffer()
+ val bufferedSink = (sink as Sink).buffer()
+ bufferedSink.writeUtf8("abc")
+ assertEquals(0, sink.size)
+ }
+
+ @Test fun bytesEmittedToSinkWithEmit() {
+ val sink = Buffer()
+ val bufferedSink = (sink as Sink).buffer()
+ bufferedSink.writeUtf8("abc")
+ bufferedSink.emit()
+ assertEquals(3, sink.size)
+ }
+
+ @Test fun completeSegmentsEmitted() {
+ val sink = Buffer()
+ val bufferedSink = (sink as Sink).buffer()
+ bufferedSink.writeUtf8("a".repeat(Segment.SIZE * 3))
+ assertEquals(Segment.SIZE.toLong() * 3L, sink.size)
+ }
+
+ @Test fun incompleteSegmentsNotEmitted() {
+ val sink = Buffer()
+ val bufferedSink = (sink as Sink).buffer()
+ bufferedSink.writeUtf8("a".repeat(Segment.SIZE * 3 - 1))
+ assertEquals(Segment.SIZE.toLong() * 2L, sink.size)
+ }
+
+ @Test fun closeWithExceptionWhenWriting() {
+ val mockSink = MockSink()
+ mockSink.scheduleThrow(0, IOException())
+ val bufferedSink = mockSink.buffer()
+ bufferedSink.writeByte('a'.toInt())
+ assertFailsWith<IOException> {
+ bufferedSink.close()
+ }
+
+ mockSink.assertLog("write([text=a], 1)", "close()")
+ }
+
+ @Test fun closeWithExceptionWhenClosing() {
+ val mockSink = MockSink()
+ mockSink.scheduleThrow(1, IOException())
+ val bufferedSink = mockSink.buffer()
+ bufferedSink.writeByte('a'.toInt())
+ assertFailsWith<IOException> {
+ bufferedSink.close()
+ }
+
+ mockSink.assertLog("write([text=a], 1)", "close()")
+ }
+
+ @Test fun closeWithExceptionWhenWritingAndClosing() {
+ val mockSink = MockSink()
+ mockSink.scheduleThrow(0, IOException("first"))
+ mockSink.scheduleThrow(1, IOException("second"))
+ val bufferedSink = mockSink.buffer()
+ bufferedSink.writeByte('a'.toInt())
+ try {
+ bufferedSink.close()
+ fail()
+ } catch (expected: IOException) {
+ assertEquals("first", expected.message)
+ }
+
+ mockSink.assertLog("write([text=a], 1)", "close()")
+ }
+
+ @Test fun operationsAfterClose() {
+ val mockSink = MockSink()
+ val bufferedSink = mockSink.buffer()
+ bufferedSink.writeByte('a'.toInt())
+ bufferedSink.close()
+
+ // Test a sample set of methods.
+ assertFailsWith<IllegalStateException> {
+ bufferedSink.writeByte('a'.toInt())
+ }
+
+ assertFailsWith<IllegalStateException> {
+ bufferedSink.write(ByteArray(10))
+ }
+
+ assertFailsWith<IllegalStateException> {
+ bufferedSink.emitCompleteSegments()
+ }
+
+ assertFailsWith<IllegalStateException> {
+ bufferedSink.emit()
+ }
+
+ assertFailsWith<IllegalStateException> {
+ bufferedSink.flush()
+ }
+ }
+
+ @Test fun writeAll() {
+ val mockSink = MockSink()
+ val bufferedSink = mockSink.buffer()
+
+ bufferedSink.buffer.writeUtf8("abc")
+ assertEquals(3, bufferedSink.writeAll(Buffer().writeUtf8("def")))
+
+ assertEquals(6, bufferedSink.buffer.size)
+ assertEquals("abcdef", bufferedSink.buffer.readUtf8(6))
+ mockSink.assertLog() // No writes.
+ }
+
+ @Test fun writeAllExhausted() {
+ val mockSink = MockSink()
+ val bufferedSink = mockSink.buffer()
+
+ assertEquals(0, bufferedSink.writeAll(Buffer()))
+ assertEquals(0, bufferedSink.buffer.size)
+ mockSink.assertLog() // No writes.
+ }
+
+ @Test fun writeAllWritesOneSegmentAtATime() {
+ val write1 = Buffer().writeUtf8("a".repeat(Segment.SIZE))
+ val write2 = Buffer().writeUtf8("b".repeat(Segment.SIZE))
+ val write3 = Buffer().writeUtf8("c".repeat(Segment.SIZE))
+
+ val source = Buffer().writeUtf8(
+ "${"a".repeat(Segment.SIZE)}${"b".repeat(Segment.SIZE)}${"c".repeat(Segment.SIZE)}"
+ )
+
+ val mockSink = MockSink()
+ val bufferedSink = mockSink.buffer()
+ assertEquals(Segment.SIZE.toLong() * 3L, bufferedSink.writeAll(source))
+
+ mockSink.assertLog(
+ "write($write1, ${write1.size})",
+ "write($write2, ${write2.size})",
+ "write($write3, ${write3.size})"
+ )
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/CommonRealBufferedSourceTest.kt b/okio/src/commonTest/kotlin/okio/CommonRealBufferedSourceTest.kt
new file mode 100644
index 00000000..4663919a
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/CommonRealBufferedSourceTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+/**
+ * Tests solely for the behavior of RealBufferedSource's implementation. For generic
+ * BufferedSource behavior use BufferedSourceTest.
+ */
+class CommonRealBufferedSourceTest {
+ @Test fun indexOfStopsReadingAtLimit() {
+ val buffer = Buffer().writeUtf8("abcdef")
+ val bufferedSource = (
+ object : Source by buffer {
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ return buffer.read(sink, minOf(1, byteCount))
+ }
+ }
+ ).buffer()
+
+ assertEquals(6, buffer.size)
+ assertEquals(-1, bufferedSource.indexOf('e'.toByte(), 0, 4))
+ assertEquals(2, buffer.size)
+ }
+
+ @Test fun requireTracksBufferFirst() {
+ val source = Buffer()
+ source.writeUtf8("bb")
+
+ val bufferedSource = (source as Source).buffer()
+ bufferedSource.buffer.writeUtf8("aa")
+
+ bufferedSource.require(2)
+ assertEquals(2, bufferedSource.buffer.size)
+ assertEquals(2, source.size)
+ }
+
+ @Test fun requireIncludesBufferBytes() {
+ val source = Buffer()
+ source.writeUtf8("b")
+
+ val bufferedSource = (source as Source).buffer()
+ bufferedSource.buffer.writeUtf8("a")
+
+ bufferedSource.require(2)
+ assertEquals("ab", bufferedSource.buffer.readUtf8(2))
+ }
+
+ @Test fun requireInsufficientData() {
+ val source = Buffer()
+ source.writeUtf8("a")
+
+ val bufferedSource = (source as Source).buffer()
+
+ assertFailsWith<EOFException> {
+ bufferedSource.require(2)
+ }
+ }
+
+ @Test fun requireReadsOneSegmentAtATime() {
+ val source = Buffer()
+ source.writeUtf8("a".repeat(Segment.SIZE))
+ source.writeUtf8("b".repeat(Segment.SIZE))
+
+ val bufferedSource = (source as Source).buffer()
+
+ bufferedSource.require(2)
+ assertEquals(Segment.SIZE.toLong(), source.size)
+ assertEquals(Segment.SIZE.toLong(), bufferedSource.buffer.size)
+ }
+
+ @Test fun skipReadsOneSegmentAtATime() {
+ val source = Buffer()
+ source.writeUtf8("a".repeat(Segment.SIZE))
+ source.writeUtf8("b".repeat(Segment.SIZE))
+ val bufferedSource = (source as Source).buffer()
+ bufferedSource.skip(2)
+ assertEquals(Segment.SIZE.toLong(), source.size)
+ assertEquals(Segment.SIZE.toLong() - 2L, bufferedSource.buffer.size)
+ }
+
+ @Test fun skipTracksBufferFirst() {
+ val source = Buffer()
+ source.writeUtf8("bb")
+
+ val bufferedSource = (source as Source).buffer()
+ bufferedSource.buffer.writeUtf8("aa")
+
+ bufferedSource.skip(2)
+ assertEquals(0, bufferedSource.buffer.size)
+ assertEquals(2, source.size)
+ }
+
+ @Test fun operationsAfterClose() {
+ val source = Buffer()
+ val bufferedSource = (source as Source).buffer()
+ bufferedSource.close()
+
+ // Test a sample set of methods.
+ assertFailsWith<IllegalStateException> {
+ bufferedSource.indexOf(1.toByte())
+ }
+
+ assertFailsWith<IllegalStateException> {
+ bufferedSource.skip(1)
+ }
+
+ assertFailsWith<IllegalStateException> {
+ bufferedSource.readByte()
+ }
+
+ assertFailsWith<IllegalStateException> {
+ bufferedSource.readByteString(10)
+ }
+ }
+
+ /**
+ * We don't want readAll to buffer an unbounded amount of data. Instead it
+ * should buffer a segment, write it, and repeat.
+ */
+ @Test fun readAllReadsOneSegmentAtATime() {
+ val write1 = Buffer().writeUtf8("a".repeat(Segment.SIZE))
+ val write2 = Buffer().writeUtf8("b".repeat(Segment.SIZE))
+ val write3 = Buffer().writeUtf8("c".repeat(Segment.SIZE))
+
+ val source = Buffer().writeUtf8(
+ "${"a".repeat(Segment.SIZE)}${"b".repeat(Segment.SIZE)}${"c".repeat(Segment.SIZE)}"
+ )
+
+ val mockSink = MockSink()
+ val bufferedSource = (source as Source).buffer()
+ assertEquals(Segment.SIZE.toLong() * 3L, bufferedSource.readAll(mockSink))
+ mockSink.assertLog(
+ "write($write1, ${write1.size})",
+ "write($write2, ${write2.size})",
+ "write($write3, ${write3.size})"
+ )
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/FakeClock.kt b/okio/src/commonTest/kotlin/okio/FakeClock.kt
new file mode 100644
index 00000000..31cf5503
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/FakeClock.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+import kotlin.time.Duration
+import kotlin.time.ExperimentalTime
+
+@ExperimentalTime
+internal class FakeClock : Clock {
+ var time = Instant.parse("2021-01-01T00:00:00Z")
+
+ override fun now() = time
+
+ fun sleep(duration: Duration) {
+ time = time.plus(duration)
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/HashingSinkTest.kt b/okio/src/commonTest/kotlin/okio/HashingSinkTest.kt
new file mode 100644
index 00000000..db1aeeec
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/HashingSinkTest.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.HashingSink.Companion.hmacSha1
+import okio.HashingSink.Companion.hmacSha256
+import okio.HashingSink.Companion.hmacSha512
+import okio.HashingSink.Companion.sha1
+import okio.HashingSink.Companion.sha256
+import okio.HashingSink.Companion.sha512
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class HashingSinkTest {
+ private val source = Buffer()
+ private val sink = Buffer()
+
+ @Test fun md5() {
+ val hashingSink: HashingSink = HashingSink.md5(sink)
+ source.writeUtf8("abc")
+ hashingSink.write(source, 3L)
+ assertEquals(HashingTest.MD5_abc, hashingSink.hash)
+ }
+
+ @Test fun sha1() {
+ val hashingSink = sha1(sink)
+ source.writeUtf8("abc")
+ hashingSink.write(source, 3L)
+ assertEquals(HashingTest.SHA1_abc, hashingSink.hash)
+ }
+
+ @Test fun sha256() {
+ val hashingSink = sha256(sink)
+ source.writeUtf8("abc")
+ hashingSink.write(source, 3L)
+ assertEquals(HashingTest.SHA256_abc, hashingSink.hash)
+ }
+
+ @Test fun sha512() {
+ val hashingSink = sha512(sink)
+ source.writeUtf8("abc")
+ hashingSink.write(source, 3L)
+ assertEquals(HashingTest.SHA512_abc, hashingSink.hash)
+ }
+
+ @Test fun hmacSha1() {
+ val hashingSink = hmacSha1(sink, HashingTest.HMAC_KEY)
+ source.writeUtf8("abc")
+ hashingSink.write(source, 3L)
+ assertEquals(HashingTest.HMAC_SHA1_abc, hashingSink.hash)
+ }
+
+ @Test fun hmacSha256() {
+ val hashingSink = hmacSha256(sink, HashingTest.HMAC_KEY)
+ source.writeUtf8("abc")
+ hashingSink.write(source, 3L)
+ assertEquals(HashingTest.HMAC_SHA256_abc, hashingSink.hash)
+ }
+
+ @Test fun hmacSha512() {
+ val hashingSink = hmacSha512(sink, HashingTest.HMAC_KEY)
+ source.writeUtf8("abc")
+ hashingSink.write(source, 3L)
+ assertEquals(HashingTest.HMAC_SHA512_abc, hashingSink.hash)
+ }
+
+ @Test fun multipleWrites() {
+ val hashingSink = sha256(sink)
+ source.writeUtf8("a")
+ hashingSink.write(source, 1L)
+ source.writeUtf8("b")
+ hashingSink.write(source, 1L)
+ source.writeUtf8("c")
+ hashingSink.write(source, 1L)
+ assertEquals(HashingTest.SHA256_abc, hashingSink.hash)
+ }
+
+ @Test fun multipleHashes() {
+ val hashingSink = sha256(sink)
+ source.writeUtf8("abc")
+ hashingSink.write(source, 3L)
+ val hash_abc = hashingSink.hash
+ assertEquals(HashingTest.SHA256_abc, hash_abc)
+ source.writeUtf8("def")
+ hashingSink.write(source, 3L)
+ assertEquals(HashingTest.SHA256_def, hashingSink.hash)
+ assertEquals(HashingTest.SHA256_abc, hash_abc)
+ }
+
+ @Test fun multipleSegments() {
+ val hashingSink = sha256(sink)
+ source.write(HashingTest.r32k)
+ hashingSink.write(source, HashingTest.r32k.size.toLong())
+ assertEquals(HashingTest.SHA256_r32k, hashingSink.hash)
+ }
+
+ @Test fun readFromPrefixOfBuffer() {
+ source.writeUtf8("z")
+ source.write(HashingTest.r32k)
+ source.skip(1)
+ source.writeUtf8("z".repeat(Segment.SIZE * 2 - 1))
+ val hashingSink = sha256(sink)
+ hashingSink.write(source, HashingTest.r32k.size.toLong())
+ assertEquals(HashingTest.SHA256_r32k, hashingSink.hash)
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/HashingSourceTest.kt b/okio/src/commonTest/kotlin/okio/HashingSourceTest.kt
new file mode 100644
index 00000000..83e2e264
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/HashingSourceTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.HashingSource.Companion.hmacSha1
+import okio.HashingSource.Companion.hmacSha256
+import okio.HashingSource.Companion.hmacSha512
+import okio.HashingSource.Companion.md5
+import okio.HashingSource.Companion.sha1
+import okio.HashingSource.Companion.sha256
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.fail
+
+class HashingSourceTest {
+ private val source = Buffer()
+ private val sink = Buffer()
+
+ @Test fun md5() {
+ val hashingSource = md5(source)
+ source.writeUtf8("abc")
+ assertEquals(3L, hashingSource.read(sink, Long.MAX_VALUE))
+ assertEquals(HashingTest.MD5_abc, hashingSource.hash)
+ }
+
+ @Test fun sha1() {
+ val hashingSource = sha1(source)
+ source.writeUtf8("abc")
+ assertEquals(3L, hashingSource.read(sink, Long.MAX_VALUE))
+ assertEquals(HashingTest.SHA1_abc, hashingSource.hash)
+ }
+
+ @Test fun sha256() {
+ val hashingSource = sha256(source)
+ source.writeUtf8("abc")
+ assertEquals(3L, hashingSource.read(sink, Long.MAX_VALUE))
+ assertEquals(HashingTest.SHA256_abc, hashingSource.hash)
+ }
+
+ @Test fun sha512() {
+ val hashingSource: HashingSource = HashingSource.sha512(source)
+ source.writeUtf8("abc")
+ assertEquals(3L, hashingSource.read(sink, Long.MAX_VALUE))
+ assertEquals(HashingTest.SHA512_abc, hashingSource.hash)
+ }
+
+ @Test fun hmacSha1() {
+ val hashingSource = hmacSha1(source, HashingTest.HMAC_KEY)
+ source.writeUtf8("abc")
+ assertEquals(3L, hashingSource.read(sink, Long.MAX_VALUE))
+ assertEquals(HashingTest.HMAC_SHA1_abc, hashingSource.hash)
+ }
+
+ @Test fun hmacSha256() {
+ val hashingSource = hmacSha256(source, HashingTest.HMAC_KEY)
+ source.writeUtf8("abc")
+ assertEquals(3L, hashingSource.read(sink, Long.MAX_VALUE))
+ assertEquals(HashingTest.HMAC_SHA256_abc, hashingSource.hash)
+ }
+
+ @Test fun hmacSha512() {
+ val hashingSource = hmacSha512(source, HashingTest.HMAC_KEY)
+ source.writeUtf8("abc")
+ assertEquals(3L, hashingSource.read(sink, Long.MAX_VALUE))
+ assertEquals(HashingTest.HMAC_SHA512_abc, hashingSource.hash)
+ }
+
+ @Test fun multipleReads() {
+ val hashingSource = sha256(source)
+ val bufferedSource = hashingSource.buffer()
+ source.writeUtf8("a")
+ assertEquals('a'.toLong(), bufferedSource.readUtf8CodePoint().toLong())
+ source.writeUtf8("b")
+ assertEquals('b'.toLong(), bufferedSource.readUtf8CodePoint().toLong())
+ source.writeUtf8("c")
+ assertEquals('c'.toLong(), bufferedSource.readUtf8CodePoint().toLong())
+ assertEquals(HashingTest.SHA256_abc, hashingSource.hash)
+ }
+
+ @Test fun multipleHashes() {
+ val hashingSource = sha256(source)
+ source.writeUtf8("abc")
+ assertEquals(3L, hashingSource.read(sink, Long.MAX_VALUE))
+ val hash_abc = hashingSource.hash
+ assertEquals(HashingTest.SHA256_abc, hash_abc)
+ source.writeUtf8("def")
+ assertEquals(3L, hashingSource.read(sink, Long.MAX_VALUE))
+ assertEquals(HashingTest.SHA256_def, hashingSource.hash)
+ assertEquals(HashingTest.SHA256_abc, hash_abc)
+ }
+
+ @Test fun multipleSegments() {
+ val hashingSource = sha256(source)
+ val bufferedSource = hashingSource.buffer()
+ source.write(HashingTest.r32k)
+ assertEquals(HashingTest.r32k, bufferedSource.readByteString())
+ assertEquals(HashingTest.SHA256_r32k, hashingSource.hash)
+ }
+
+ @Test fun readIntoSuffixOfBuffer() {
+ val hashingSource = sha256(source)
+ source.write(HashingTest.r32k)
+ sink.writeUtf8("z".repeat(Segment.SIZE * 2 - 1))
+ assertEquals(HashingTest.r32k.size.toLong(), hashingSource.read(sink, Long.MAX_VALUE))
+ assertEquals(HashingTest.SHA256_r32k, hashingSource.hash)
+ }
+
+ @Test fun hmacEmptyKey() {
+ try {
+ hmacSha256(source, ByteString.EMPTY)
+ fail()
+ } catch (expected: IllegalArgumentException) {
+ }
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/HashingTest.kt b/okio/src/commonTest/kotlin/okio/HashingTest.kt
new file mode 100644
index 00000000..1cae58d0
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/HashingTest.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2014 Square Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.ByteString.Companion.decodeHex
+import okio.ByteString.Companion.encodeUtf8
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class HashingTest {
+ @Test fun byteStringMd5() {
+ assertEquals(MD5_abc, "abc".encodeUtf8().md5())
+ }
+
+ @Test fun byteStringSha1() {
+ assertEquals(SHA1_abc, "abc".encodeUtf8().sha1())
+ }
+
+ @Test fun byteStringSha256() {
+ assertEquals(SHA256_abc, "abc".encodeUtf8().sha256())
+ }
+
+ @Test fun byteStringSha512() {
+ assertEquals(SHA512_abc, "abc".encodeUtf8().sha512())
+ }
+
+ @Test fun byteStringHmacSha1() {
+ assertEquals(HMAC_SHA1_abc, "abc".encodeUtf8().hmacSha1(HMAC_KEY))
+ }
+
+ @Test fun byteStringHmacSha256() {
+ assertEquals(HMAC_SHA256_abc, "abc".encodeUtf8().hmacSha256(HMAC_KEY))
+ }
+
+ @Test fun byteStringHmacSha512() {
+ assertEquals(HMAC_SHA512_abc, "abc".encodeUtf8().hmacSha512(HMAC_KEY))
+ }
+
+ @Test fun bufferMd5() {
+ assertEquals(MD5_abc, Buffer().writeUtf8("abc").md5())
+ }
+
+ @Test fun bufferSha1() {
+ assertEquals(SHA1_abc, Buffer().writeUtf8("abc").sha1())
+ }
+
+ @Test fun bufferSha256() {
+ assertEquals(SHA256_abc, Buffer().writeUtf8("abc").sha256())
+ }
+
+ @Test fun bufferSha512() {
+ assertEquals(SHA512_abc, Buffer().writeUtf8("abc").sha512())
+ }
+
+ @Test fun hashEmptySha256Buffer() {
+ assertEquals(SHA256_empty, Buffer().sha256())
+ }
+
+ @Test fun hashEmptySha512Buffer() {
+ assertEquals(SHA512_empty, Buffer().sha512())
+ }
+
+ @Test fun bufferHmacSha1() {
+ assertEquals(HMAC_SHA1_abc, Buffer().writeUtf8("abc").hmacSha1(HMAC_KEY))
+ }
+
+ @Test fun bufferHmacSha256() {
+ assertEquals(HMAC_SHA256_abc, Buffer().writeUtf8("abc").hmacSha256(HMAC_KEY))
+ }
+
+ @Test fun bufferHmacSha512() {
+ assertEquals(HMAC_SHA512_abc, Buffer().writeUtf8("abc").hmacSha512(HMAC_KEY))
+ }
+
+ @Test fun hmacSha256EmptyBuffer() {
+ assertEquals(HMAC_SHA256_empty, Buffer().sha256())
+ }
+
+ @Test fun hmacSha512EmptyBuffer() {
+ assertEquals(HMAC_SHA512_empty, Buffer().sha512())
+ }
+
+ @Test fun bufferHashIsNotDestructive() {
+ val buffer = Buffer()
+
+ buffer.writeUtf8("abc")
+ assertEquals(SHA256_abc, buffer.sha256())
+ assertEquals("abc", buffer.readUtf8())
+
+ buffer.writeUtf8("def")
+ assertEquals(SHA256_def, buffer.sha256())
+ assertEquals("def", buffer.readUtf8())
+
+ buffer.write(r32k)
+ assertEquals(SHA256_r32k, buffer.sha256())
+ assertEquals(r32k, buffer.readByteString())
+ }
+
+ companion object {
+ val HMAC_KEY =
+ "0102030405060708".decodeHex()
+ val MD5_abc =
+ "900150983cd24fb0d6963f7d28e17f72".decodeHex()
+ val SHA1_abc =
+ "a9993e364706816aba3e25717850c26c9cd0d89d".decodeHex()
+ val HMAC_SHA1_abc =
+ "987af8649982ff7d9fbb1b8aa35099146997af51".decodeHex()
+ val SHA256_abc =
+ "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad".decodeHex()
+ val SHA256_empty =
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".decodeHex()
+ val SHA256_def =
+ "cb8379ac2098aa165029e3938a51da0bcecfc008fd6795f401178647f96c5b34".decodeHex()
+ val SHA256_r32k =
+ "dadec7297f49bdf219895bd9942454047d394e1f20f247fbdc591080b4e8731e".decodeHex()
+ val SHA512_abc =
+ "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f".decodeHex()
+ val SHA512_empty =
+ "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e".decodeHex()
+ val HMAC_SHA256_empty =
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".decodeHex()
+ val HMAC_SHA256_abc =
+ "446d1715583cf1c30dfffbec0df4ff1f9d39d493211ab4c97ed6f3f0eb579b47".decodeHex()
+ val HMAC_SHA512_empty =
+ "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e".decodeHex()
+ val HMAC_SHA512_abc =
+ "24391790e7131050b05b606f2079a8983313894a1642a5ed97d094e7cabd00cfaa857d92c1f320ca3b6aaabb84c7155d6f1b10940dc133ded1b40baee8900be6".decodeHex()
+ val r32k = randomBytes(32768)
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/MockSink.kt b/okio/src/commonTest/kotlin/okio/MockSink.kt
new file mode 100644
index 00000000..7e099f42
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/MockSink.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+/** A scriptable sink. Like Mockito, but worse and requiring less configuration. */
+class MockSink : Sink {
+ private val log = mutableListOf<String>()
+ private val callThrows = mutableMapOf<Int, IOException>()
+
+ fun assertLog(vararg messages: String) {
+ assertEquals(messages.toList(), log)
+ }
+
+ fun assertLogContains(message: String) {
+ assertTrue(message in log)
+ }
+
+ fun scheduleThrow(call: Int, e: IOException) {
+ callThrows[call] = e
+ }
+
+ private fun throwIfScheduled() {
+ val exception = callThrows[log.size - 1]
+ if (exception != null) throw exception
+ }
+
+ override fun write(source: Buffer, byteCount: Long) {
+ log.add("write($source, $byteCount)")
+ source.skip(byteCount)
+ throwIfScheduled()
+ }
+
+ override fun flush() {
+ log.add("flush()")
+ throwIfScheduled()
+ }
+
+ override fun timeout(): Timeout {
+ log.add("timeout()")
+ return Timeout.NONE
+ }
+
+ override fun close() {
+ log.add("close()")
+ throwIfScheduled()
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/UnsafeCursorTest.kt b/okio/src/commonTest/kotlin/okio/UnsafeCursorTest.kt
new file mode 100644
index 00000000..680ebdc2
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/UnsafeCursorTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class UnsafeCursorTest {
+ @Test fun acquireForRead() {
+ val buffer = Buffer()
+ buffer.writeUtf8("xo".repeat(5000))
+
+ val cursor = buffer.readAndWriteUnsafe()
+ try {
+ val copy = Buffer()
+ while (cursor.next() != -1) {
+ copy.write(cursor.data!!, cursor.start, cursor.end - cursor.start)
+ }
+ } finally {
+ cursor.close()
+ }
+
+ assertEquals("xo".repeat(5000), buffer.readUtf8())
+ }
+
+ @Test fun acquireForWrite() {
+ val buffer = Buffer()
+ buffer.writeUtf8("xo".repeat(5000))
+
+ val cursor = buffer.readAndWriteUnsafe()
+ try {
+ while (cursor.next() != -1) {
+ cursor.data!!.fill('z'.toByte(), cursor.start, cursor.end)
+ }
+ } finally {
+ cursor.close()
+ }
+
+ assertEquals("zz".repeat(5000), buffer.readUtf8())
+ }
+
+ @Test fun expand() {
+ val buffer = Buffer()
+
+ val cursor = buffer.readAndWriteUnsafe()
+ try {
+ cursor.expandBuffer(100)
+ cursor.data!!.fill('z'.toByte(), cursor.start, cursor.start + 100)
+ cursor.resizeBuffer(100L)
+ } finally {
+ cursor.close()
+ }
+
+ val expected = "z".repeat(100)
+ val actual = buffer.readUtf8()
+ println(actual)
+ println(expected)
+ assertEquals(expected, actual)
+ }
+
+ @Test fun resizeBuffer() {
+ val buffer = Buffer()
+
+ val cursor = buffer.readAndWriteUnsafe()
+ try {
+ cursor.resizeBuffer(100L)
+ cursor.data!!.fill('z'.toByte(), cursor.start, cursor.end)
+ } finally {
+ cursor.close()
+ }
+
+ assertEquals("z".repeat(100), buffer.readUtf8())
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/Utf8KotlinTest.kt b/okio/src/commonTest/kotlin/okio/Utf8KotlinTest.kt
new file mode 100644
index 00000000..1ade4179
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/Utf8KotlinTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.ByteString.Companion.decodeHex
+import okio.internal.commonAsUtf8ToByteArray
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class Utf8KotlinTest {
+ @Test fun oneByteCharacters() {
+ assertEncoded("00", 0x00) // Smallest 1-byte character.
+ assertEncoded("20", ' '.toInt())
+ assertEncoded("7e", '~'.toInt())
+ assertEncoded("7f", 0x7f) // Largest 1-byte character.
+ }
+
+ @Test fun twoByteCharacters() {
+ assertEncoded("c280", 0x0080) // Smallest 2-byte character.
+ assertEncoded("c3bf", 0x00ff)
+ assertEncoded("c480", 0x0100)
+ assertEncoded("dfbf", 0x07ff) // Largest 2-byte character.
+ }
+
+ @Test fun threeByteCharacters() {
+ assertEncoded("e0a080", 0x0800) // Smallest 3-byte character.
+ assertEncoded("e0bfbf", 0x0fff)
+ assertEncoded("e18080", 0x1000)
+ assertEncoded("e1bfbf", 0x1fff)
+ assertEncoded("ed8080", 0xd000)
+ assertEncoded("ed9fbf", 0xd7ff) // Largest character lower than the min surrogate.
+ assertEncoded("ee8080", 0xe000) // Smallest character greater than the max surrogate.
+ assertEncoded("eebfbf", 0xefff)
+ assertEncoded("ef8080", 0xf000)
+ assertEncoded("efbfbf", 0xffff) // Largest 3-byte character.
+ }
+
+ @Test fun fourByteCharacters() {
+ assertEncoded("f0908080", 0x010000) // Smallest surrogate pair.
+ assertEncoded("f48fbfbf", 0x10ffff) // Largest code point expressible by UTF-16.
+ }
+
+ @Test fun unknownBytes() {
+ assertCodePointDecoded("f8", REPLACEMENT_CODE_POINT) // Too large
+ assertCodePointDecoded("f0f8", REPLACEMENT_CODE_POINT, REPLACEMENT_CODE_POINT)
+ assertCodePointDecoded("ff", REPLACEMENT_CODE_POINT) // Largest
+ assertCodePointDecoded("f0ff", REPLACEMENT_CODE_POINT, REPLACEMENT_CODE_POINT)
+
+ // Lone continuation
+ assertCodePointDecoded("80", REPLACEMENT_CODE_POINT) // Smallest
+ assertCodePointDecoded("bf", REPLACEMENT_CODE_POINT) // Largest
+ }
+
+ @Test fun overlongSequences() {
+ // Overlong representation of the NUL character
+ assertCodePointDecoded("c080", REPLACEMENT_CODE_POINT)
+ assertCodePointDecoded("e08080", REPLACEMENT_CODE_POINT)
+ assertCodePointDecoded("f0808080", REPLACEMENT_CODE_POINT)
+
+ // Maximum overlong sequences
+ assertCodePointDecoded("c1bf", REPLACEMENT_CODE_POINT)
+ assertCodePointDecoded("e09fbf", REPLACEMENT_CODE_POINT)
+ assertCodePointDecoded("f08fbfbf", REPLACEMENT_CODE_POINT)
+ }
+
+ @Test fun danglingHighSurrogate() {
+ assertStringEncoded("3f", "\ud800") // "?"
+ assertCodePointDecoded("eda080", REPLACEMENT_CODE_POINT)
+ }
+
+ @Test fun lowSurrogateWithoutHighSurrogate() {
+ assertStringEncoded("3f", "\udc00") // "?"
+ assertCodePointDecoded("edb080", REPLACEMENT_CODE_POINT)
+ }
+
+ @Test fun highSurrogateFollowedByNonSurrogate() {
+ assertStringEncoded("3fee8080", "\ud800\ue000") // "?\ue000": Following character is too high.
+ assertCodePointDecoded("f090ee8080", REPLACEMENT_CODE_POINT, '\ue000'.toInt())
+
+ assertStringEncoded("3f61", "\ud800\u0061") // "?a": Following character is too low.
+ assertCodePointDecoded("f09061", REPLACEMENT_CODE_POINT, 'a'.toInt())
+ }
+
+ @Test fun doubleLowSurrogate() {
+ assertStringEncoded("3f3f", "\udc00\udc00") // "??"
+ assertCodePointDecoded("edb080edb080", REPLACEMENT_CODE_POINT, REPLACEMENT_CODE_POINT)
+ }
+
+ @Test fun doubleHighSurrogate() {
+ assertStringEncoded("3f3f", "\ud800\ud800") // "??"
+ assertCodePointDecoded("eda080eda080", REPLACEMENT_CODE_POINT, REPLACEMENT_CODE_POINT)
+ }
+
+ @Test fun lowSurrogateHighSurrogate() {
+ assertStringEncoded("3f3f", "\udc00\ud800") // "??"
+ assertCodePointDecoded("edb080eda080", REPLACEMENT_CODE_POINT, REPLACEMENT_CODE_POINT)
+ }
+
+ @Test fun writeSurrogateCodePoint() {
+ assertStringEncoded("ed9fbf", "\ud7ff") // Below lowest surrogate is okay.
+ assertCodePointDecoded("ed9fbf", '\ud7ff'.toInt())
+
+ assertStringEncoded("3f", "\ud800") // Lowest surrogate gets '?'.
+ assertCodePointDecoded("eda080", REPLACEMENT_CODE_POINT)
+
+ assertStringEncoded("3f", "\udfff") // Highest surrogate gets '?'.
+ assertCodePointDecoded("edbfbf", REPLACEMENT_CODE_POINT)
+
+ assertStringEncoded("ee8080", "\ue000") // Above highest surrogate is okay.
+ assertCodePointDecoded("ee8080", '\ue000'.toInt())
+ }
+
+ @Test fun size() {
+ assertEquals(0, "".utf8Size())
+ assertEquals(3, "abc".utf8Size())
+ assertEquals(16, "təˈranəˌsôr".utf8Size())
+ }
+
+ @Test fun sizeWithBounds() {
+ assertEquals(0, "".utf8Size(0, 0))
+ assertEquals(0, "abc".utf8Size(0, 0))
+ assertEquals(1, "abc".utf8Size(1, 2))
+ assertEquals(2, "abc".utf8Size(0, 2))
+ assertEquals(3, "abc".utf8Size(0, 3))
+ assertEquals(16, "təˈranəˌsôr".utf8Size(0, 11))
+ assertEquals(5, "təˈranəˌsôr".utf8Size(3, 7))
+ }
+
+ @Test fun sizeBoundsCheck() {
+ assertFailsWith<IllegalArgumentException> {
+ "abc".utf8Size(-1, 2)
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ "abc".utf8Size(2, 1)
+ }
+
+ assertFailsWith<IllegalArgumentException> {
+ "abc".utf8Size(1, 4)
+ }
+ }
+
+ private fun assertEncoded(hex: String, vararg codePoints: Int) {
+ assertCodePointDecoded(hex, *codePoints)
+ }
+
+ private fun assertCodePointDecoded(hex: String, vararg codePoints: Int) {
+ val bytes = hex.decodeHex().toByteArray()
+ var i = 0
+ bytes.processUtf8CodePoints(0, bytes.size) { codePoint ->
+ if (i < codePoints.size) assertEquals(codePoints[i], codePoint, "index=$i")
+ i++
+ }
+ assertEquals(i, codePoints.size) // Checked them all
+ }
+
+ private fun assertStringEncoded(hex: String, string: String) {
+ val expectedUtf8 = hex.decodeHex()
+
+ // Confirm our expectations are consistent with the platform.
+ val platformUtf8 = ByteString.of(*string.asUtf8ToByteArray())
+ assertEquals(expectedUtf8, platformUtf8)
+
+ // Confirm our implementations matches those expectations.
+ val actualUtf8 = ByteString.of(*string.commonAsUtf8ToByteArray())
+ assertEquals(expectedUtf8, actualUtf8)
+
+ // TODO Confirm we are consistent when writing one code point at a time.
+
+ // Confirm we are consistent when measuring lengths.
+ assertEquals(expectedUtf8.size.toLong(), string.utf8Size())
+ assertEquals(expectedUtf8.size.toLong(), string.utf8Size(0, string.length))
+ }
+}
diff --git a/okio/src/commonTest/kotlin/okio/util.kt b/okio/src/commonTest/kotlin/okio/util.kt
new file mode 100644
index 00000000..953eef88
--- /dev/null
+++ b/okio/src/commonTest/kotlin/okio/util.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import kotlin.random.Random
+import kotlin.test.assertEquals
+
+fun Char.repeat(count: Int): String {
+ return toString().repeat(count)
+}
+
+fun segmentSizes(buffer: Buffer): List<Int> {
+ var segment = buffer.head ?: return emptyList()
+
+ val sizes = mutableListOf(segment.limit - segment.pos)
+ segment = segment.next!!
+ while (segment !== buffer.head) {
+ sizes.add(segment.limit - segment.pos)
+ segment = segment.next!!
+ }
+ return sizes
+}
+
+fun assertArrayEquals(a: ByteArray, b: ByteArray) {
+ assertEquals(a.contentToString(), b.contentToString())
+}
+
+fun randomBytes(length: Int): ByteString {
+ val random = Random(0)
+ val randomBytes = ByteArray(length)
+ random.nextBytes(randomBytes)
+ return ByteString.of(*randomBytes)
+}
+
+fun bufferWithRandomSegmentLayout(dice: Random, data: ByteArray): Buffer {
+ val result = Buffer()
+
+ // Writing to result directly will yield packed segments. Instead, write to
+ // other buffers, then write those buffers to result.
+ var pos = 0
+ var byteCount: Int
+ while (pos < data.size) {
+ byteCount = Segment.SIZE / 2 + dice.nextInt(Segment.SIZE / 2)
+ if (byteCount > data.size - pos) byteCount = data.size - pos
+ val offset = dice.nextInt(Segment.SIZE - byteCount)
+
+ val segment = Buffer()
+ segment.write(ByteArray(offset))
+ segment.write(data, pos, byteCount)
+ segment.skip(offset.toLong())
+
+ result.write(segment, byteCount.toLong())
+ pos += byteCount
+ }
+
+ return result
+}
+
+fun bufferWithSegments(vararg segments: String): Buffer {
+ val result = Buffer()
+ for (s in segments) {
+ val offsetInSegment = if (s.length < Segment.SIZE) (Segment.SIZE - s.length) / 2 else 0
+ val buffer = Buffer()
+ buffer.writeUtf8('_'.repeat(offsetInSegment))
+ buffer.writeUtf8(s)
+ buffer.skip(offsetInSegment.toLong())
+ result.write(buffer.copyTo(Buffer()), buffer.size)
+ }
+ return result
+}
+
+fun makeSegments(source: ByteString): ByteString {
+ val buffer = Buffer()
+ for (i in 0 until source.size) {
+ val segment = buffer.writableSegment(Segment.SIZE)
+ segment.data[segment.pos] = source[i]
+ segment.limit++
+ buffer.size++
+ }
+ return buffer.snapshot()
+}
diff --git a/okio/src/hashFunctions/kotlin/okio/internal/HashFunction.kt b/okio/src/hashFunctions/kotlin/okio/internal/HashFunction.kt
new file mode 100644
index 00000000..5948ab0f
--- /dev/null
+++ b/okio/src/hashFunctions/kotlin/okio/internal/HashFunction.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.internal
+
+/** A cryptographic hash function. */
+internal interface HashFunction {
+ fun update(
+ input: ByteArray,
+ offset: Int = 0,
+ byteCount: Int = input.size
+ )
+
+ fun digest(): ByteArray
+}
diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Hmac.kt b/okio/src/hashFunctions/kotlin/okio/internal/Hmac.kt
new file mode 100644
index 00000000..95e3c5dd
--- /dev/null
+++ b/okio/src/hashFunctions/kotlin/okio/internal/Hmac.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.internal
+
+import okio.ByteString
+import okio.xor
+
+internal class Hmac private constructor(
+ private val hashFunction: HashFunction,
+ private val outerKey: ByteArray
+) : HashFunction {
+ override fun update(input: ByteArray, offset: Int, byteCount: Int) {
+ hashFunction.update(input, offset, byteCount)
+ }
+
+ override fun digest(): ByteArray {
+ val digest = hashFunction.digest()
+
+ hashFunction.update(outerKey)
+ hashFunction.update(digest)
+
+ return hashFunction.digest()
+ }
+
+ companion object {
+ private const val IPAD: Byte = 54
+ private const val OPAD: Byte = 92
+
+ fun sha1(key: ByteString) =
+ create(key, hashFunction = Sha1(), blockLength = 64)
+
+ fun sha256(key: ByteString) =
+ create(key, hashFunction = Sha256(), blockLength = 64)
+
+ fun sha512(key: ByteString) =
+ create(key, hashFunction = Sha512(), blockLength = 128)
+
+ private fun create(
+ key: ByteString,
+ hashFunction: HashFunction,
+ blockLength: Int
+ ): Hmac {
+ val keySize = key.size
+ val paddedKey = when {
+ keySize == 0 -> throw IllegalArgumentException("Empty key")
+ keySize == blockLength -> key.data
+ keySize < blockLength -> key.data.copyOf(blockLength)
+ else -> hashFunction.apply { update(key.data) }.digest().copyOf(blockLength)
+ }
+
+ val innerKey = ByteArray(blockLength) { paddedKey[it] xor IPAD }
+ val outerKey = ByteArray(blockLength) { paddedKey[it] xor OPAD }
+
+ hashFunction.update(innerKey)
+
+ return Hmac(
+ hashFunction,
+ outerKey
+ )
+ }
+ }
+}
diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Md5.kt b/okio/src/hashFunctions/kotlin/okio/internal/Md5.kt
new file mode 100644
index 00000000..e43e4476
--- /dev/null
+++ b/okio/src/hashFunctions/kotlin/okio/internal/Md5.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.internal
+
+import okio.leftRotate
+
+internal class Md5 : HashFunction {
+ private var messageLength = 0L
+ private val unprocessed = ByteArray(64)
+ private var unprocessedLimit = 0
+ private val words = IntArray(16)
+
+ private var h0: Int = 1732584193
+ private var h1: Int = -271733879
+ private var h2: Int = -1732584194
+ private var h3: Int = 271733878
+
+ override fun update(
+ input: ByteArray,
+ offset: Int,
+ byteCount: Int
+ ) {
+ messageLength += byteCount
+ var pos = offset
+ val limit = pos + byteCount
+ val unprocessed = this.unprocessed
+ val unprocessedLimit = this.unprocessedLimit
+
+ if (unprocessedLimit > 0) {
+ if (unprocessedLimit + byteCount < 64) {
+ // Not enough bytes for a chunk.
+ input.copyInto(unprocessed, unprocessedLimit, pos, limit)
+ this.unprocessedLimit = unprocessedLimit + byteCount
+ return
+ }
+
+ // Process a chunk combining leftover bytes and the input.
+ val consumeByteCount = 64 - unprocessedLimit
+ input.copyInto(unprocessed, unprocessedLimit, pos, pos + consumeByteCount)
+ processChunk(unprocessed, 0)
+ this.unprocessedLimit = 0
+ pos += consumeByteCount
+ }
+
+ while (pos < limit) {
+ val nextPos = pos + 64
+
+ if (nextPos > limit) {
+ // Not enough bytes for a chunk.
+ input.copyInto(unprocessed, 0, pos, limit)
+ this.unprocessedLimit = limit - pos
+ return
+ }
+
+ // Process a chunk.
+ processChunk(input, pos)
+ pos = nextPos
+ }
+ }
+
+ private fun processChunk(input: ByteArray, pos: Int) {
+ val words = this.words
+
+ var pos = pos
+ for (w in 0 until 16) {
+ words[w] = ((input[pos++].toInt() and 0xff)) or
+ ((input[pos++].toInt() and 0xff) shl 8) or
+ ((input[pos++].toInt() and 0xff) shl 16) or
+ ((input[pos++].toInt() and 0xff) shl 24)
+ }
+
+ hash(words)
+ }
+
+ private fun hash(words: IntArray) {
+ val localK = k
+ val localS = s
+
+ var a = h0
+ var b = h1
+ var c = h2
+ var d = h3
+
+ for (i in 0 until 16) {
+ val g = i
+ val f = ((b and c) or (b.inv() and d)) + a + localK[i] + words[g]
+ a = d
+ d = c
+ c = b
+ b += f leftRotate localS[i]
+ }
+
+ for (i in 16 until 32) {
+ val g = ((5 * i) + 1) % 16
+ val f = ((d and b) or (d.inv() and c)) + a + localK[i] + words[g]
+ a = d
+ d = c
+ c = b
+ b += f leftRotate localS[i]
+ }
+
+ for (i in 32 until 48) {
+ val g = ((3 * i) + 5) % 16
+ val f = (b xor c xor d) + a + localK[i] + words[g]
+ a = d
+ d = c
+ c = b
+ b += f leftRotate localS[i]
+ }
+
+ for (i in 48 until 64) {
+ val g = (7 * i) % 16
+ val f = (c xor (b or d.inv())) + a + localK[i] + words[g]
+ a = d
+ d = c
+ c = b
+ b += f leftRotate localS[i]
+ }
+
+ h0 += a
+ h1 += b
+ h2 += c
+ h3 += d
+ }
+
+ /* ktlint-disable */
+ override fun digest(): ByteArray {
+ val messageLengthBits = messageLength * 8
+
+ unprocessed[unprocessedLimit++] = 0x80.toByte()
+ if (unprocessedLimit > 56) {
+ unprocessed.fill(0, unprocessedLimit, 64)
+ processChunk(unprocessed, 0)
+ unprocessed.fill(0, 0, unprocessedLimit)
+ } else {
+ unprocessed.fill(0, unprocessedLimit, 56)
+ }
+ unprocessed[56] = (messageLengthBits ).toByte()
+ unprocessed[57] = (messageLengthBits ushr 8).toByte()
+ unprocessed[58] = (messageLengthBits ushr 16).toByte()
+ unprocessed[59] = (messageLengthBits ushr 24).toByte()
+ unprocessed[60] = (messageLengthBits ushr 32).toByte()
+ unprocessed[61] = (messageLengthBits ushr 40).toByte()
+ unprocessed[62] = (messageLengthBits ushr 48).toByte()
+ unprocessed[63] = (messageLengthBits ushr 56).toByte()
+ processChunk(unprocessed, 0)
+
+ val a = h0
+ val b = h1
+ val c = h2
+ val d = h3
+
+ return byteArrayOf(
+ (a ).toByte(),
+ (a shr 8).toByte(),
+ (a shr 16).toByte(),
+ (a shr 24).toByte(),
+ (b ).toByte(),
+ (b shr 8).toByte(),
+ (b shr 16).toByte(),
+ (b shr 24).toByte(),
+ (c ).toByte(),
+ (c shr 8).toByte(),
+ (c shr 16).toByte(),
+ (c shr 24).toByte(),
+ (d ).toByte(),
+ (d shr 8).toByte(),
+ (d shr 16).toByte(),
+ (d shr 24).toByte()
+ )
+ }
+ /* ktlint-enable */
+
+ companion object {
+ private val s = intArrayOf(
+ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
+ 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15,
+ 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
+ )
+
+ private val k = intArrayOf(
+ -680876936, -389564586, 606105819, -1044525330, -176418897, 1200080426, -1473231341,
+ -45705983, 1770035416, -1958414417, -42063, -1990404162, 1804603682, -40341101, -1502002290,
+ 1236535329, -165796510, -1069501632, 643717713, -373897302, -701558691, 38016083, -660478335,
+ -405537848, 568446438, -1019803690, -187363961, 1163531501, -1444681467, -51403784,
+ 1735328473, -1926607734, -378558, -2022574463, 1839030562, -35309556, -1530992060, 1272893353,
+ -155497632, -1094730640, 681279174, -358537222, -722521979, 76029189, -640364487, -421815835,
+ 530742520, -995338651, -198630844, 1126891415, -1416354905, -57434055, 1700485571,
+ -1894986606, -1051523, -2054922799, 1873313359, -30611744, -1560198380, 1309151649,
+ -145523070, -1120210379, 718787259, -343485551
+ )
+ }
+}
diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Sha1.kt b/okio/src/hashFunctions/kotlin/okio/internal/Sha1.kt
new file mode 100644
index 00000000..e9a8de16
--- /dev/null
+++ b/okio/src/hashFunctions/kotlin/okio/internal/Sha1.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.internal
+
+import okio.leftRotate
+
+internal class Sha1 : HashFunction {
+ private var messageLength = 0L
+ private val unprocessed = ByteArray(64)
+ private var unprocessedLimit = 0
+ private val words = IntArray(80)
+
+ private var h0 = 1732584193
+ private var h1 = -271733879
+ private var h2 = -1732584194
+ private var h3 = 271733878
+ private var h4 = -1009589776
+
+ override fun update(
+ input: ByteArray,
+ offset: Int,
+ byteCount: Int
+ ) {
+ messageLength += byteCount
+ var pos = offset
+ val limit = pos + byteCount
+ val unprocessed = this.unprocessed
+ val unprocessedLimit = this.unprocessedLimit
+
+ if (unprocessedLimit > 0) {
+ if (unprocessedLimit + byteCount < 64) {
+ // Not enough bytes for a chunk.
+ input.copyInto(unprocessed, unprocessedLimit, pos, limit)
+ this.unprocessedLimit = unprocessedLimit + byteCount
+ return
+ }
+
+ // Process a chunk combining leftover bytes and the input.
+ val consumeByteCount = 64 - unprocessedLimit
+ input.copyInto(unprocessed, unprocessedLimit, pos, pos + consumeByteCount)
+ processChunk(unprocessed, 0)
+ this.unprocessedLimit = 0
+ pos += consumeByteCount
+ }
+
+ while (pos < limit) {
+ val nextPos = pos + 64
+
+ if (nextPos > limit) {
+ // Not enough bytes for a chunk.
+ input.copyInto(unprocessed, 0, pos, limit)
+ this.unprocessedLimit = limit - pos
+ return
+ }
+
+ // Process a chunk.
+ processChunk(input, pos)
+ pos = nextPos
+ }
+ }
+
+ private fun processChunk(input: ByteArray, pos: Int) {
+ val words = this.words
+
+ var pos = pos
+ for (w in 0 until 16) {
+ words[w] =
+ ((input[pos++].toInt() and 0xff) shl 24) or
+ ((input[pos++].toInt() and 0xff) shl 16) or
+ ((input[pos++].toInt() and 0xff) shl 8) or
+ ((input[pos++].toInt() and 0xff))
+ }
+
+ for (w in 16 until 80) {
+ words[w] = (words[w - 3] xor words[w - 8] xor words[w - 14] xor words[w - 16]) leftRotate 1
+ }
+
+ var a = h0
+ var b = h1
+ var c = h2
+ var d = h3
+ var e = h4
+
+ for (i in 0 until 80) {
+ val a2 = when {
+ i < 20 -> {
+ val f = d xor (b and (c xor d))
+ val k = 1518500249
+ (a leftRotate 5) + f + e + k + words[i]
+ }
+ i < 40 -> {
+ val f = b xor c xor d
+ val k = 1859775393
+ (a leftRotate 5) + f + e + k + words[i]
+ }
+ i < 60 -> {
+ val f = (b and c) or (b and d) or (c and d)
+ val k = -1894007588
+ (a leftRotate 5) + f + e + k + words[i]
+ }
+ else -> {
+ val f = b xor c xor d
+ val k = -899497514
+ (a leftRotate 5) + f + e + k + words[i]
+ }
+ }
+
+ e = d
+ d = c
+ c = b leftRotate 30
+ b = a
+ a = a2
+ }
+
+ h0 += a
+ h1 += b
+ h2 += c
+ h3 += d
+ h4 += e
+ }
+
+ /* ktlint-disable */
+ override fun digest(): ByteArray {
+ val unprocessed = this.unprocessed
+ var unprocessedLimit = this.unprocessedLimit
+ val messageLengthBits = messageLength * 8
+
+ unprocessed[unprocessedLimit++] = 0x80.toByte()
+ if (unprocessedLimit > 56) {
+ unprocessed.fill(0, unprocessedLimit, 64)
+ processChunk(unprocessed, 0)
+ unprocessed.fill(0, 0, unprocessedLimit)
+ } else {
+ unprocessed.fill(0, unprocessedLimit, 56)
+ }
+ unprocessed[56] = (messageLengthBits ushr 56).toByte()
+ unprocessed[57] = (messageLengthBits ushr 48).toByte()
+ unprocessed[58] = (messageLengthBits ushr 40).toByte()
+ unprocessed[59] = (messageLengthBits ushr 32).toByte()
+ unprocessed[60] = (messageLengthBits ushr 24).toByte()
+ unprocessed[61] = (messageLengthBits ushr 16).toByte()
+ unprocessed[62] = (messageLengthBits ushr 8).toByte()
+ unprocessed[63] = (messageLengthBits ).toByte()
+ processChunk(unprocessed, 0)
+
+ val a = h0
+ val b = h1
+ val c = h2
+ val d = h3
+ val e = h4
+
+ reset()
+
+ return byteArrayOf(
+ (a shr 24).toByte(),
+ (a shr 16).toByte(),
+ (a shr 8).toByte(),
+ (a ).toByte(),
+ (b shr 24).toByte(),
+ (b shr 16).toByte(),
+ (b shr 8).toByte(),
+ (b ).toByte(),
+ (c shr 24).toByte(),
+ (c shr 16).toByte(),
+ (c shr 8).toByte(),
+ (c ).toByte(),
+ (d shr 24).toByte(),
+ (d shr 16).toByte(),
+ (d shr 8).toByte(),
+ (d ).toByte(),
+ (e shr 24).toByte(),
+ (e shr 16).toByte(),
+ (e shr 8).toByte(),
+ (e ).toByte()
+ )
+ }
+ /* ktlint-enable */
+
+ private fun reset() {
+ messageLength = 0L
+ unprocessed.fill(0)
+ unprocessedLimit = 0
+ words.fill(0)
+
+ h0 = 1732584193
+ h1 = -271733879
+ h2 = -1732584194
+ h3 = 271733878
+ h4 = -1009589776
+ }
+}
diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Sha256.kt b/okio/src/hashFunctions/kotlin/okio/internal/Sha256.kt
new file mode 100644
index 00000000..aa0d24d0
--- /dev/null
+++ b/okio/src/hashFunctions/kotlin/okio/internal/Sha256.kt
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.internal
+
+import okio.and
+
+internal class Sha256 : HashFunction {
+ private var messageLength = 0L
+ private val unprocessed = ByteArray(64)
+ private var unprocessedLimit = 0
+ private val words = IntArray(64)
+
+ private var h0 = 1779033703
+ private var h1 = -1150833019
+ private var h2 = 1013904242
+ private var h3 = -1521486534
+ private var h4 = 1359893119
+ private var h5 = -1694144372
+ private var h6 = 528734635
+ private var h7 = 1541459225
+
+ override fun update(
+ input: ByteArray,
+ offset: Int,
+ byteCount: Int
+ ) {
+ messageLength += byteCount
+ var pos = offset
+ val limit = pos + byteCount
+ val unprocessed = this.unprocessed
+ val unprocessedLimit = this.unprocessedLimit
+
+ if (unprocessedLimit > 0) {
+ if (unprocessedLimit + byteCount < 64) {
+ // Not enough bytes for a chunk.
+ input.copyInto(unprocessed, unprocessedLimit, pos, limit)
+ this.unprocessedLimit = unprocessedLimit + byteCount
+ return
+ }
+
+ // Process a chunk combining leftover bytes and the input.
+ val consumeByteCount = 64 - unprocessedLimit
+ input.copyInto(unprocessed, unprocessedLimit, pos, pos + consumeByteCount)
+ processChunk(unprocessed, 0)
+ this.unprocessedLimit = 0
+ pos += consumeByteCount
+ }
+
+ while (pos < limit) {
+ val nextPos = pos + 64
+
+ if (nextPos > limit) {
+ // Not enough bytes for a chunk.
+ input.copyInto(unprocessed, 0, pos, limit)
+ this.unprocessedLimit = limit - pos
+ return
+ }
+
+ // Process a chunk.
+ processChunk(input, pos)
+ pos = nextPos
+ }
+ }
+
+ private fun processChunk(input: ByteArray, pos: Int) {
+ val words = this.words
+
+ var pos = pos
+ for (w in 0 until 16) {
+ words[w] = ((input[pos++] and 0xff) shl 24) or
+ ((input[pos++] and 0xff) shl 16) or
+ ((input[pos++] and 0xff) shl 8) or
+ ((input[pos++] and 0xff))
+ }
+
+ for (w in 16 until 64) {
+ val w15 = words[w - 15]
+ val s0 = ((w15 ushr 7) or (w15 shl 25)) xor ((w15 ushr 18) or (w15 shl 14)) xor (w15 ushr 3)
+ val w2 = words[w - 2]
+ val s1 = ((w2 ushr 17) or (w2 shl 15)) xor ((w2 ushr 19) or (w2 shl 13)) xor (w2 ushr 10)
+ val w16 = words[w - 16]
+ val w7 = words[w - 7]
+ words[w] = w16 + s0 + w7 + s1
+ }
+
+ hash(words)
+ }
+
+ private fun hash(
+ words: IntArray
+ ) {
+ val localK = k
+ var a = h0
+ var b = h1
+ var c = h2
+ var d = h3
+ var e = h4
+ var f = h5
+ var g = h6
+ var h = h7
+
+ for (i in 0 until 64) {
+ val s0 = ((a ushr 2) or (a shl 30)) xor
+ ((a ushr 13) or (a shl 19)) xor
+ ((a ushr 22) or (a shl 10))
+ val s1 = ((e ushr 6) or (e shl 26)) xor
+ ((e ushr 11) or (e shl 21)) xor
+ ((e ushr 25) or (e shl 7))
+
+ val ch = (e and f) xor
+ (e.inv() and g)
+ val maj = (a and b) xor
+ (a and c) xor
+ (b and c)
+
+ val t1 = h + s1 + ch + localK[i] + words[i]
+ val t2 = s0 + maj
+
+ h = g
+ g = f
+ f = e
+ e = d + t1
+ d = c
+ c = b
+ b = a
+ a = t1 + t2
+ }
+
+ h0 += a
+ h1 += b
+ h2 += c
+ h3 += d
+ h4 += e
+ h5 += f
+ h6 += g
+ h7 += h
+ }
+
+ /* ktlint-disable */
+ override fun digest(): ByteArray {
+ val unprocessed = this.unprocessed
+ var unprocessedLimit = this.unprocessedLimit
+ val messageLengthBits = messageLength * 8
+
+ unprocessed[unprocessedLimit++] = 0x80.toByte()
+ if (unprocessedLimit > 56) {
+ unprocessed.fill(0, unprocessedLimit, 64)
+ processChunk(unprocessed, 0)
+ unprocessed.fill(0, 0, unprocessedLimit)
+ } else {
+ unprocessed.fill(0, unprocessedLimit, 56)
+ }
+ unprocessed[56] = (messageLengthBits ushr 56).toByte()
+ unprocessed[57] = (messageLengthBits ushr 48).toByte()
+ unprocessed[58] = (messageLengthBits ushr 40).toByte()
+ unprocessed[59] = (messageLengthBits ushr 32).toByte()
+ unprocessed[60] = (messageLengthBits ushr 24).toByte()
+ unprocessed[61] = (messageLengthBits ushr 16).toByte()
+ unprocessed[62] = (messageLengthBits ushr 8).toByte()
+ unprocessed[63] = (messageLengthBits ).toByte()
+ processChunk(unprocessed, 0)
+
+ val a = h0
+ val b = h1
+ val c = h2
+ val d = h3
+ val e = h4
+ val f = h5
+ val g = h6
+ val h = h7
+
+ reset()
+
+ return byteArrayOf(
+ (a shr 24).toByte(),
+ (a shr 16).toByte(),
+ (a shr 8).toByte(),
+ (a ).toByte(),
+ (b shr 24).toByte(),
+ (b shr 16).toByte(),
+ (b shr 8).toByte(),
+ (b ).toByte(),
+ (c shr 24).toByte(),
+ (c shr 16).toByte(),
+ (c shr 8).toByte(),
+ (c ).toByte(),
+ (d shr 24).toByte(),
+ (d shr 16).toByte(),
+ (d shr 8).toByte(),
+ (d ).toByte(),
+ (e shr 24).toByte(),
+ (e shr 16).toByte(),
+ (e shr 8).toByte(),
+ (e ).toByte(),
+ (f shr 24).toByte(),
+ (f shr 16).toByte(),
+ (f shr 8).toByte(),
+ (f ).toByte(),
+ (g shr 24).toByte(),
+ (g shr 16).toByte(),
+ (g shr 8).toByte(),
+ (g ).toByte(),
+ (h shr 24).toByte(),
+ (h shr 16).toByte(),
+ (h shr 8).toByte(),
+ (h ).toByte()
+ )
+ }
+ /* ktlint-enable */
+
+ private fun reset() {
+ messageLength = 0L
+ unprocessed.fill(0)
+ unprocessedLimit = 0
+ words.fill(0)
+
+ h0 = 1779033703
+ h1 = -1150833019
+ h2 = 1013904242
+ h3 = -1521486534
+ h4 = 1359893119
+ h5 = -1694144372
+ h6 = 528734635
+ h7 = 1541459225
+ }
+
+ companion object {
+ private val k = intArrayOf(
+ 1116352408, 1899447441, -1245643825, -373957723, 961987163, 1508970993, -1841331548,
+ -1424204075, -670586216, 310598401, 607225278, 1426881987, 1925078388, -2132889090,
+ -1680079193, -1046744716, -459576895, -272742522, 264347078, 604807628, 770255983, 1249150122,
+ 1555081692, 1996064986, -1740746414, -1473132947, -1341970488, -1084653625, -958395405,
+ -710438585, 113926993, 338241895, 666307205, 773529912, 1294757372, 1396182291, 1695183700,
+ 1986661051, -2117940946, -1838011259, -1564481375, -1474664885, -1035236496, -949202525,
+ -778901479, -694614492, -200395387, 275423344, 430227734, 506948616, 659060556, 883997877,
+ 958139571, 1322822218, 1537002063, 1747873779, 1955562222, 2024104815, -2067236844,
+ -1933114872, -1866530822, -1538233109, -1090935817, -965641998
+ )
+ }
+}
diff --git a/okio/src/hashFunctions/kotlin/okio/internal/Sha512.kt b/okio/src/hashFunctions/kotlin/okio/internal/Sha512.kt
new file mode 100644
index 00000000..390a50a1
--- /dev/null
+++ b/okio/src/hashFunctions/kotlin/okio/internal/Sha512.kt
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.internal
+
+import okio.rightRotate
+
+internal class Sha512 : HashFunction {
+ private var messageLength = 0L
+ private val unprocessed = ByteArray(128)
+ private var unprocessedLimit = 0
+ private val words = LongArray(80)
+
+ private var h0 = 7640891576956012808L
+ private var h1 = -4942790177534073029L
+ private var h2 = 4354685564936845355L
+ private var h3 = -6534734903238641935L
+ private var h4 = 5840696475078001361L
+ private var h5 = -7276294671716946913L
+ private var h6 = 2270897969802886507L
+ private var h7 = 6620516959819538809L
+
+ override fun update(
+ input: ByteArray,
+ offset: Int,
+ byteCount: Int
+ ) {
+ messageLength += byteCount
+ var pos = offset
+ val limit = pos + byteCount
+ val unprocessed = this.unprocessed
+ val unprocessedLimit = this.unprocessedLimit
+
+ if (unprocessedLimit > 0) {
+ if (unprocessedLimit + byteCount < 128) {
+ // Not enough bytes for a chunk.
+ input.copyInto(unprocessed, unprocessedLimit, pos, limit)
+ this.unprocessedLimit = unprocessedLimit + byteCount
+ return
+ }
+
+ // Process a chunk combining leftover bytes and the input.
+ val consumeByteCount = 128 - unprocessedLimit
+ input.copyInto(unprocessed, unprocessedLimit, pos, pos + consumeByteCount)
+ processChunk(unprocessed, 0)
+ this.unprocessedLimit = 0
+ pos += consumeByteCount
+ }
+
+ while (pos < limit) {
+ val nextPos = pos + 128
+
+ if (nextPos > limit) {
+ // Not enough bytes for a chunk.
+ input.copyInto(unprocessed, 0, pos, limit)
+ this.unprocessedLimit = limit - pos
+ return
+ }
+
+ // Process a chunk.
+ processChunk(input, pos)
+ pos = nextPos
+ }
+ }
+
+ private fun processChunk(input: ByteArray, pos: Int) {
+ val words = this.words
+
+ var pos = pos
+ for (w in 0 until 16) {
+ words[w] = ((input[pos++].toLong() and 0xff) shl 56) or
+ ((input[pos++].toLong() and 0xff) shl 48) or
+ ((input[pos++].toLong() and 0xff) shl 40) or
+ ((input[pos++].toLong() and 0xff) shl 32) or
+ ((input[pos++].toLong() and 0xff) shl 24) or
+ ((input[pos++].toLong() and 0xff) shl 16) or
+ ((input[pos++].toLong() and 0xff) shl 8) or
+ ((input[pos++].toLong() and 0xff))
+ }
+
+ for (i in 16 until 80) {
+ val w15 = words[i - 15]
+ val s0 = (w15 rightRotate 1) xor (w15 rightRotate 8) xor (w15 ushr 7)
+ val w2 = words[i - 2]
+ val s1 = (w2 rightRotate 19) xor (w2 rightRotate 61) xor (w2 ushr 6)
+ val w16 = words[i - 16]
+ val w7 = words[i - 7]
+ words[i] = w16 + s0 + w7 + s1
+ }
+
+ hash(words)
+ }
+
+ private fun hash(words: LongArray) {
+ val localK = k
+ var a = h0
+ var b = h1
+ var c = h2
+ var d = h3
+ var e = h4
+ var f = h5
+ var g = h6
+ var h = h7
+
+ for (i in 0 until 80) {
+ val s0 = (a rightRotate 28) xor (a rightRotate 34) xor (a rightRotate 39)
+ val s1 = (e rightRotate 14) xor (e rightRotate 18) xor (e rightRotate 41)
+
+ val ch = (e and f) xor (e.inv() and g)
+ val maj = (a and b) xor (a and c) xor (b and c)
+
+ val t1 = h + s1 + ch + localK[i] + words[i]
+ val t2 = s0 + maj
+
+ h = g
+ g = f
+ f = e
+ e = d + t1
+ d = c
+ c = b
+ b = a
+ a = t1 + t2
+ }
+
+ h0 += a
+ h1 += b
+ h2 += c
+ h3 += d
+ h4 += e
+ h5 += f
+ h6 += g
+ h7 += h
+ }
+
+ /* ktlint-disable */
+ override fun digest(): ByteArray {
+ val unprocessed = this.unprocessed
+ var unprocessedLimit = this.unprocessedLimit
+ val messageLengthBits = messageLength * 8
+
+ unprocessed[unprocessedLimit++] = 0x80.toByte()
+ if (unprocessedLimit > 112) {
+ unprocessed.fill(0, unprocessedLimit, 128)
+ processChunk(unprocessed, 0)
+ unprocessed.fill(0, 0, unprocessedLimit)
+ } else {
+ unprocessed.fill(0, unprocessedLimit, 120)
+ }
+ unprocessed[120] = (messageLengthBits ushr 56).toByte()
+ unprocessed[121] = (messageLengthBits ushr 48).toByte()
+ unprocessed[122] = (messageLengthBits ushr 40).toByte()
+ unprocessed[123] = (messageLengthBits ushr 32).toByte()
+ unprocessed[124] = (messageLengthBits ushr 24).toByte()
+ unprocessed[125] = (messageLengthBits ushr 16).toByte()
+ unprocessed[126] = (messageLengthBits ushr 8).toByte()
+ unprocessed[127] = (messageLengthBits ).toByte()
+ processChunk(unprocessed, 0)
+
+ val a = h0
+ val b = h1
+ val c = h2
+ val d = h3
+ val e = h4
+ val f = h5
+ val g = h6
+ val h = h7
+
+ reset()
+
+ return byteArrayOf(
+ (a shr 56).toByte(),
+ (a shr 48).toByte(),
+ (a shr 40).toByte(),
+ (a shr 32).toByte(),
+ (a shr 24).toByte(),
+ (a shr 16).toByte(),
+ (a shr 8).toByte(),
+ (a ).toByte(),
+ (b shr 56).toByte(),
+ (b shr 48).toByte(),
+ (b shr 40).toByte(),
+ (b shr 32).toByte(),
+ (b shr 24).toByte(),
+ (b shr 16).toByte(),
+ (b shr 8).toByte(),
+ (b ).toByte(),
+ (c shr 56).toByte(),
+ (c shr 48).toByte(),
+ (c shr 40).toByte(),
+ (c shr 32).toByte(),
+ (c shr 24).toByte(),
+ (c shr 16).toByte(),
+ (c shr 8).toByte(),
+ (c ).toByte(),
+ (d shr 56).toByte(),
+ (d shr 48).toByte(),
+ (d shr 40).toByte(),
+ (d shr 32).toByte(),
+ (d shr 24).toByte(),
+ (d shr 16).toByte(),
+ (d shr 8).toByte(),
+ (d ).toByte(),
+ (e shr 56).toByte(),
+ (e shr 48).toByte(),
+ (e shr 40).toByte(),
+ (e shr 32).toByte(),
+ (e shr 24).toByte(),
+ (e shr 16).toByte(),
+ (e shr 8).toByte(),
+ (e ).toByte(),
+ (f shr 56).toByte(),
+ (f shr 48).toByte(),
+ (f shr 40).toByte(),
+ (f shr 32).toByte(),
+ (f shr 24).toByte(),
+ (f shr 16).toByte(),
+ (f shr 8).toByte(),
+ (f ).toByte(),
+ (g shr 56).toByte(),
+ (g shr 48).toByte(),
+ (g shr 40).toByte(),
+ (g shr 32).toByte(),
+ (g shr 24).toByte(),
+ (g shr 16).toByte(),
+ (g shr 8).toByte(),
+ (g ).toByte(),
+ (h shr 56).toByte(),
+ (h shr 48).toByte(),
+ (h shr 40).toByte(),
+ (h shr 32).toByte(),
+ (h shr 24).toByte(),
+ (h shr 16).toByte(),
+ (h shr 8).toByte(),
+ (h ).toByte()
+ )
+ }
+ /* ktlint-enable */
+
+ private fun reset() {
+ messageLength = 0L
+ unprocessed.fill(0)
+ unprocessedLimit = 0
+ words.fill(0)
+
+ h0 = 7640891576956012808L
+ h1 = -4942790177534073029L
+ h2 = 4354685564936845355L
+ h3 = -6534734903238641935L
+ h4 = 5840696475078001361L
+ h5 = -7276294671716946913L
+ h6 = 2270897969802886507L
+ h7 = 6620516959819538809L
+ }
+
+ companion object {
+ private val k = longArrayOf(
+ 4794697086780616226L, 8158064640168781261L, -5349999486874862801L, -1606136188198331460L,
+ 4131703408338449720L, 6480981068601479193L, -7908458776815382629L, -6116909921290321640L,
+ -2880145864133508542L, 1334009975649890238L, 2608012711638119052L, 6128411473006802146L,
+ 8268148722764581231L, -9160688886553864527L, -7215885187991268811L, -4495734319001033068L,
+ -1973867731355612462L, -1171420211273849373L, 1135362057144423861L, 2597628984639134821L,
+ 3308224258029322869L, 5365058923640841347L, 6679025012923562964L, 8573033837759648693L,
+ -7476448914759557205L, -6327057829258317296L, -5763719355590565569L, -4658551843659510044L,
+ -4116276920077217854L, -3051310485924567259L, 489312712824947311L, 1452737877330783856L,
+ 2861767655752347644L, 3322285676063803686L, 5560940570517711597L, 5996557281743188959L,
+ 7280758554555802590L, 8532644243296465576L, -9096487096722542874L, -7894198246740708037L,
+ -6719396339535248540L, -6333637450476146687L, -4446306890439682159L, -4076793802049405392L,
+ -3345356375505022440L, -2983346525034927856L, -860691631967231958L, 1182934255886127544L,
+ 1847814050463011016L, 2177327727835720531L, 2830643537854262169L, 3796741975233480872L,
+ 4115178125766777443L, 5681478168544905931L, 6601373596472566643L, 7507060721942968483L,
+ 8399075790359081724L, 8693463985226723168L, -8878714635349349518L, -8302665154208450068L,
+ -8016688836872298968L, -6606660893046293015L, -4685533653050689259L, -4147400797238176981L,
+ -3880063495543823972L, -3348786107499101689L, -1523767162380948706L, -757361751448694408L,
+ 500013540394364858L, 748580250866718886L, 1242879168328830382L, 1977374033974150939L,
+ 2944078676154940804L, 3659926193048069267L, 4368137639120453308L, 4836135668995329356L,
+ 5532061633213252278L, 6448918945643986474L, 6902733635092675308L, 7801388544844847127L
+ )
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/-DeprecatedOkio.kt b/okio/src/jvmMain/kotlin/okio/-DeprecatedOkio.kt
new file mode 100644
index 00000000..7b6835e1
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/-DeprecatedOkio.kt
@@ -0,0 +1,147 @@
+// ktlint-disable filename
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.net.Socket
+import java.nio.file.OpenOption
+import java.nio.file.Path
+
+@Deprecated(message = "changed in Okio 2.x")
+object `-DeprecatedOkio` {
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "file.appendingSink()",
+ imports = ["okio.appendingSink"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun appendingSink(file: File) = file.appendingSink()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "sink.buffer()",
+ imports = ["okio.buffer"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun buffer(sink: Sink) = sink.buffer()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "source.buffer()",
+ imports = ["okio.buffer"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun buffer(source: Source) = source.buffer()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "file.sink()",
+ imports = ["okio.sink"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun sink(file: File) = file.sink()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "outputStream.sink()",
+ imports = ["okio.sink"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun sink(outputStream: OutputStream) = outputStream.sink()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "path.sink(*options)",
+ imports = ["okio.sink"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun sink(path: Path, vararg options: OpenOption) = path.sink(*options)
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "socket.sink()",
+ imports = ["okio.sink"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun sink(socket: Socket) = socket.sink()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "file.source()",
+ imports = ["okio.source"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun source(file: File) = file.source()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "inputStream.source()",
+ imports = ["okio.source"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun source(inputStream: InputStream) = inputStream.source()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "path.source(*options)",
+ imports = ["okio.source"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun source(path: Path, vararg options: OpenOption) = path.source(*options)
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "socket.source()",
+ imports = ["okio.source"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun source(socket: Socket) = socket.source()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "blackholeSink()",
+ imports = ["okio.blackholeSink"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun blackhole() = blackholeSink()
+}
diff --git a/okio/src/jvmMain/kotlin/okio/-DeprecatedUpgrade.kt b/okio/src/jvmMain/kotlin/okio/-DeprecatedUpgrade.kt
new file mode 100644
index 00000000..5f955470
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/-DeprecatedUpgrade.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:JvmName("-DeprecatedUpgrade")
+package okio
+
+val Okio = `-DeprecatedOkio`
+val Utf8 = `-DeprecatedUtf8`
diff --git a/okio/src/jvmMain/kotlin/okio/-DeprecatedUtf8.kt b/okio/src/jvmMain/kotlin/okio/-DeprecatedUtf8.kt
new file mode 100644
index 00000000..b4bc7574
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/-DeprecatedUtf8.kt
@@ -0,0 +1,40 @@
+// ktlint-disable filename
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+@Deprecated(message = "changed in Okio 2.x")
+object `-DeprecatedUtf8` {
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "string.utf8Size()",
+ imports = ["okio.utf8Size"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun size(string: String) = string.utf8Size()
+
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "string.utf8Size(beginIndex, endIndex)",
+ imports = ["okio.utf8Size"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun size(string: String, beginIndex: Int, endIndex: Int) = string.utf8Size(beginIndex, endIndex)
+}
diff --git a/okio/src/jvmMain/kotlin/okio/-Platform.kt b/okio/src/jvmMain/kotlin/okio/-Platform.kt
new file mode 100644
index 00000000..4edb3ce0
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/-Platform.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("-Platform")
+package okio
+
+internal actual fun ByteArray.toUtf8String(): String = String(this, Charsets.UTF_8)
+
+internal actual fun String.asUtf8ToByteArray(): ByteArray = toByteArray(Charsets.UTF_8)
+
+// TODO remove if https://youtrack.jetbrains.com/issue/KT-20641 provides a better solution
+actual typealias ArrayIndexOutOfBoundsException = java.lang.ArrayIndexOutOfBoundsException
+
+internal actual inline fun <R> synchronized(lock: Any, block: () -> R): R {
+ return kotlin.synchronized(lock, block)
+}
+
+actual typealias IOException = java.io.IOException
+
+actual typealias EOFException = java.io.EOFException
+
+actual typealias FileNotFoundException = java.io.FileNotFoundException
+
+actual typealias Closeable = java.io.Closeable
diff --git a/okio/src/jvmMain/kotlin/okio/AsyncTimeout.kt b/okio/src/jvmMain/kotlin/okio/AsyncTimeout.kt
new file mode 100644
index 00000000..20778327
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/AsyncTimeout.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import java.io.InterruptedIOException
+import java.util.concurrent.TimeUnit
+
+/**
+ * This timeout uses a background thread to take action exactly when the timeout occurs. Use this to
+ * implement timeouts where they aren't supported natively, such as to sockets that are blocked on
+ * writing.
+ *
+ * Subclasses should override [timedOut] to take action when a timeout occurs. This method will be
+ * invoked by the shared watchdog thread so it should not do any long-running operations. Otherwise
+ * we risk starving other timeouts from being triggered.
+ *
+ * Use [sink] and [source] to apply this timeout to a stream. The returned value will apply the
+ * timeout to each operation on the wrapped stream.
+ *
+ * Callers should call [enter] before doing work that is subject to timeouts, and [exit] afterwards.
+ * The return value of [exit] indicates whether a timeout was triggered. Note that the call to
+ * [timedOut] is asynchronous, and may be called after [exit].
+ */
+open class AsyncTimeout : Timeout() {
+ /** True if this node is currently in the queue. */
+ private var inQueue = false
+
+ /** The next node in the linked list. */
+ private var next: AsyncTimeout? = null
+
+ /** If scheduled, this is the time that the watchdog should time this out. */
+ private var timeoutAt = 0L
+
+ fun enter() {
+ val timeoutNanos = timeoutNanos()
+ val hasDeadline = hasDeadline()
+ if (timeoutNanos == 0L && !hasDeadline) {
+ return // No timeout and no deadline? Don't bother with the queue.
+ }
+ scheduleTimeout(this, timeoutNanos, hasDeadline)
+ }
+
+ /** Returns true if the timeout occurred. */
+ fun exit(): Boolean {
+ return cancelScheduledTimeout(this)
+ }
+
+ /**
+ * Returns the amount of time left until the time out. This will be negative if the timeout has
+ * elapsed and the timeout should occur immediately.
+ */
+ private fun remainingNanos(now: Long) = timeoutAt - now
+
+ /**
+ * Invoked by the watchdog thread when the time between calls to [enter] and [exit] has exceeded
+ * the timeout.
+ */
+ protected open fun timedOut() {}
+
+ /**
+ * Returns a new sink that delegates to [sink], using this to implement timeouts. This works
+ * best if [timedOut] is overridden to interrupt [sink]'s current operation.
+ */
+ fun sink(sink: Sink): Sink {
+ return object : Sink {
+ override fun write(source: Buffer, byteCount: Long) {
+ checkOffsetAndCount(source.size, 0, byteCount)
+
+ var remaining = byteCount
+ while (remaining > 0L) {
+ // Count how many bytes to write. This loop guarantees we split on a segment boundary.
+ var toWrite = 0L
+ var s = source.head!!
+ while (toWrite < TIMEOUT_WRITE_SIZE) {
+ val segmentSize = s.limit - s.pos
+ toWrite += segmentSize.toLong()
+ if (toWrite >= remaining) {
+ toWrite = remaining
+ break
+ }
+ s = s.next!!
+ }
+
+ // Emit one write. Only this section is subject to the timeout.
+ withTimeout { sink.write(source, toWrite) }
+ remaining -= toWrite
+ }
+ }
+
+ override fun flush() {
+ withTimeout { sink.flush() }
+ }
+
+ override fun close() {
+ withTimeout { sink.close() }
+ }
+
+ override fun timeout() = this@AsyncTimeout
+
+ override fun toString() = "AsyncTimeout.sink($sink)"
+ }
+ }
+
+ /**
+ * Returns a new source that delegates to [source], using this to implement timeouts. This works
+ * best if [timedOut] is overridden to interrupt [source]'s current operation.
+ */
+ fun source(source: Source): Source {
+ return object : Source {
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ return withTimeout { source.read(sink, byteCount) }
+ }
+
+ override fun close() {
+ withTimeout { source.close() }
+ }
+
+ override fun timeout() = this@AsyncTimeout
+
+ override fun toString() = "AsyncTimeout.source($source)"
+ }
+ }
+
+ /**
+ * Surrounds [block] with calls to [enter] and [exit], throwing an exception from
+ * [newTimeoutException] if a timeout occurred.
+ */
+ inline fun <T> withTimeout(block: () -> T): T {
+ var throwOnTimeout = false
+ enter()
+ try {
+ val result = block()
+ throwOnTimeout = true
+ return result
+ } catch (e: IOException) {
+ throw if (!exit()) e else `access$newTimeoutException`(e)
+ } finally {
+ val timedOut = exit()
+ if (timedOut && throwOnTimeout) throw `access$newTimeoutException`(null)
+ }
+ }
+
+ @PublishedApi // Binary compatible trampoline function
+ internal fun `access$newTimeoutException`(cause: IOException?) = newTimeoutException(cause)
+
+ /**
+ * Returns an [IOException] to represent a timeout. By default this method returns
+ * [InterruptedIOException]. If [cause] is non-null it is set as the cause of the
+ * returned exception.
+ */
+ protected open fun newTimeoutException(cause: IOException?): IOException {
+ val e = InterruptedIOException("timeout")
+ if (cause != null) {
+ e.initCause(cause)
+ }
+ return e
+ }
+
+ private class Watchdog internal constructor() : Thread("Okio Watchdog") {
+ init {
+ isDaemon = true
+ }
+
+ override fun run() {
+ while (true) {
+ try {
+ var timedOut: AsyncTimeout? = null
+ synchronized(AsyncTimeout::class.java) {
+ timedOut = awaitTimeout()
+
+ // The queue is completely empty. Let this thread exit and let another watchdog thread
+ // get created on the next call to scheduleTimeout().
+ if (timedOut === head) {
+ head = null
+ return
+ }
+ }
+
+ // Close the timed out node, if one was found.
+ timedOut?.timedOut()
+ } catch (ignored: InterruptedException) {
+ }
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Don't write more than 64 KiB of data at a time, give or take a segment. Otherwise slow
+ * connections may suffer timeouts even when they're making (slow) progress. Without this,
+ * writing a single 1 MiB buffer may never succeed on a sufficiently slow connection.
+ */
+ private const val TIMEOUT_WRITE_SIZE = 64 * 1024
+
+ /** Duration for the watchdog thread to be idle before it shuts itself down. */
+ private val IDLE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(60)
+ private val IDLE_TIMEOUT_NANOS = TimeUnit.MILLISECONDS.toNanos(IDLE_TIMEOUT_MILLIS)
+
+ /**
+ * The watchdog thread processes a linked list of pending timeouts, sorted in the order to be
+ * triggered. This class synchronizes on AsyncTimeout.class. This lock guards the queue.
+ *
+ * Head's 'next' points to the first element of the linked list. The first element is the next
+ * node to time out, or null if the queue is empty. The head is null until the watchdog thread
+ * is started and also after being idle for [AsyncTimeout.IDLE_TIMEOUT_MILLIS].
+ */
+ private var head: AsyncTimeout? = null
+
+ private fun scheduleTimeout(node: AsyncTimeout, timeoutNanos: Long, hasDeadline: Boolean) {
+ synchronized(AsyncTimeout::class.java) {
+ check(!node.inQueue) { "Unbalanced enter/exit" }
+ node.inQueue = true
+
+ // Start the watchdog thread and create the head node when the first timeout is scheduled.
+ if (head == null) {
+ head = AsyncTimeout()
+ Watchdog().start()
+ }
+
+ val now = System.nanoTime()
+ if (timeoutNanos != 0L && hasDeadline) {
+ // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap
+ // around, minOf() is undefined for absolute values, but meaningful for relative ones.
+ node.timeoutAt = now + minOf(timeoutNanos, node.deadlineNanoTime() - now)
+ } else if (timeoutNanos != 0L) {
+ node.timeoutAt = now + timeoutNanos
+ } else if (hasDeadline) {
+ node.timeoutAt = node.deadlineNanoTime()
+ } else {
+ throw AssertionError()
+ }
+
+ // Insert the node in sorted order.
+ val remainingNanos = node.remainingNanos(now)
+ var prev = head!!
+ while (true) {
+ if (prev.next == null || remainingNanos < prev.next!!.remainingNanos(now)) {
+ node.next = prev.next
+ prev.next = node
+ if (prev === head) {
+ // Wake up the watchdog when inserting at the front.
+ (AsyncTimeout::class.java as Object).notify()
+ }
+ break
+ }
+ prev = prev.next!!
+ }
+ }
+ }
+
+ /** Returns true if the timeout occurred. */
+ private fun cancelScheduledTimeout(node: AsyncTimeout): Boolean {
+ synchronized(AsyncTimeout::class.java) {
+ if (!node.inQueue) return false
+ node.inQueue = false
+
+ // Remove the node from the linked list.
+ var prev = head
+ while (prev != null) {
+ if (prev.next === node) {
+ prev.next = node.next
+ node.next = null
+ return false
+ }
+ prev = prev.next
+ }
+
+ // The node wasn't found in the linked list: it must have timed out!
+ return true
+ }
+ }
+
+ /**
+ * Removes and returns the node at the head of the list, waiting for it to time out if
+ * necessary. This returns [head] if there was no node at the head of the list when starting,
+ * and there continues to be no node after waiting [IDLE_TIMEOUT_NANOS]. It returns null if a
+ * new node was inserted while waiting. Otherwise this returns the node being waited on that has
+ * been removed.
+ */
+ @Throws(InterruptedException::class)
+ internal fun awaitTimeout(): AsyncTimeout? {
+ // Get the next eligible node.
+ val node = head!!.next
+
+ // The queue is empty. Wait until either something is enqueued or the idle timeout elapses.
+ if (node == null) {
+ val startNanos = System.nanoTime()
+ (AsyncTimeout::class.java as Object).wait(IDLE_TIMEOUT_MILLIS)
+ return if (head!!.next == null && System.nanoTime() - startNanos >= IDLE_TIMEOUT_NANOS) {
+ head // The idle timeout elapsed.
+ } else {
+ null // The situation has changed.
+ }
+ }
+
+ var waitNanos = node.remainingNanos(System.nanoTime())
+
+ // The head of the queue hasn't timed out yet. Await that.
+ if (waitNanos > 0) {
+ // Waiting is made complicated by the fact that we work in nanoseconds,
+ // but the API wants (millis, nanos) in two arguments.
+ val waitMillis = waitNanos / 1000000L
+ waitNanos -= waitMillis * 1000000L
+ (AsyncTimeout::class.java as Object).wait(waitMillis, waitNanos.toInt())
+ return null
+ }
+
+ // The head of the queue has timed out. Remove it.
+ head!!.next = node.next
+ node.next = null
+ return node
+ }
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/Buffer.kt b/okio/src/jvmMain/kotlin/okio/Buffer.kt
new file mode 100644
index 00000000..857c983e
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/Buffer.kt
@@ -0,0 +1,622 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.commonClear
+import okio.internal.commonClose
+import okio.internal.commonCompleteSegmentByteCount
+import okio.internal.commonCopy
+import okio.internal.commonCopyTo
+import okio.internal.commonEquals
+import okio.internal.commonExpandBuffer
+import okio.internal.commonGet
+import okio.internal.commonHashCode
+import okio.internal.commonIndexOf
+import okio.internal.commonIndexOfElement
+import okio.internal.commonNext
+import okio.internal.commonRangeEquals
+import okio.internal.commonRead
+import okio.internal.commonReadAll
+import okio.internal.commonReadAndWriteUnsafe
+import okio.internal.commonReadByte
+import okio.internal.commonReadByteArray
+import okio.internal.commonReadByteString
+import okio.internal.commonReadDecimalLong
+import okio.internal.commonReadFully
+import okio.internal.commonReadHexadecimalUnsignedLong
+import okio.internal.commonReadInt
+import okio.internal.commonReadLong
+import okio.internal.commonReadShort
+import okio.internal.commonReadUnsafe
+import okio.internal.commonReadUtf8CodePoint
+import okio.internal.commonReadUtf8Line
+import okio.internal.commonReadUtf8LineStrict
+import okio.internal.commonResizeBuffer
+import okio.internal.commonSeek
+import okio.internal.commonSelect
+import okio.internal.commonSkip
+import okio.internal.commonSnapshot
+import okio.internal.commonWritableSegment
+import okio.internal.commonWrite
+import okio.internal.commonWriteAll
+import okio.internal.commonWriteByte
+import okio.internal.commonWriteDecimalLong
+import okio.internal.commonWriteHexadecimalUnsignedLong
+import okio.internal.commonWriteInt
+import okio.internal.commonWriteLong
+import okio.internal.commonWriteShort
+import okio.internal.commonWriteUtf8
+import okio.internal.commonWriteUtf8CodePoint
+import java.io.Closeable
+import java.io.EOFException
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import java.nio.channels.ByteChannel
+import java.nio.charset.Charset
+import java.security.InvalidKeyException
+import java.security.MessageDigest
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+
+actual class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel {
+ @JvmField internal actual var head: Segment? = null
+
+ @get:JvmName("size")
+ actual var size: Long = 0L
+ internal set
+
+ override fun buffer() = this
+
+ actual override val buffer get() = this
+
+ override fun outputStream(): OutputStream {
+ return object : OutputStream() {
+ override fun write(b: Int) {
+ writeByte(b)
+ }
+
+ override fun write(data: ByteArray, offset: Int, byteCount: Int) {
+ this@Buffer.write(data, offset, byteCount)
+ }
+
+ override fun flush() {}
+
+ override fun close() {}
+
+ override fun toString(): String = "${this@Buffer}.outputStream()"
+ }
+ }
+
+ actual override fun emitCompleteSegments() = this // Nowhere to emit to!
+
+ actual override fun emit() = this // Nowhere to emit to!
+
+ override fun exhausted() = size == 0L
+
+ @Throws(EOFException::class)
+ override fun require(byteCount: Long) {
+ if (size < byteCount) throw EOFException()
+ }
+
+ override fun request(byteCount: Long) = size >= byteCount
+
+ override fun peek(): BufferedSource {
+ return PeekSource(this).buffer()
+ }
+
+ override fun inputStream(): InputStream {
+ return object : InputStream() {
+ override fun read(): Int {
+ return if (size > 0L) {
+ readByte() and 0xff
+ } else {
+ -1
+ }
+ }
+
+ override fun read(sink: ByteArray, offset: Int, byteCount: Int): Int {
+ return this@Buffer.read(sink, offset, byteCount)
+ }
+
+ override fun available() = minOf(size, Integer.MAX_VALUE).toInt()
+
+ override fun close() {}
+
+ override fun toString() = "${this@Buffer}.inputStream()"
+ }
+ }
+
+ /** Copy `byteCount` bytes from this, starting at `offset`, to `out`. */
+ @Throws(IOException::class)
+ @JvmOverloads
+ fun copyTo(
+ out: OutputStream,
+ offset: Long = 0L,
+ byteCount: Long = size - offset
+ ): Buffer {
+ var offset = offset
+ var byteCount = byteCount
+ checkOffsetAndCount(size, offset, byteCount)
+ if (byteCount == 0L) return this
+
+ // Skip segments that we aren't copying from.
+ var s = head
+ while (offset >= s!!.limit - s.pos) {
+ offset -= (s.limit - s.pos).toLong()
+ s = s.next
+ }
+
+ // Copy from one segment at a time.
+ while (byteCount > 0L) {
+ val pos = (s!!.pos + offset).toInt()
+ val toCopy = minOf(s.limit - pos, byteCount).toInt()
+ out.write(s.data, pos, toCopy)
+ byteCount -= toCopy.toLong()
+ offset = 0L
+ s = s.next
+ }
+
+ return this
+ }
+
+ actual fun copyTo(
+ out: Buffer,
+ offset: Long,
+ byteCount: Long
+ ): Buffer = commonCopyTo(out, offset, byteCount)
+
+ actual fun copyTo(
+ out: Buffer,
+ offset: Long
+ ): Buffer = copyTo(out, offset, size - offset)
+
+ /** Write `byteCount` bytes from this to `out`. */
+ @Throws(IOException::class)
+ @JvmOverloads
+ fun writeTo(out: OutputStream, byteCount: Long = size): Buffer {
+ var byteCount = byteCount
+ checkOffsetAndCount(size, 0, byteCount)
+
+ var s = head
+ while (byteCount > 0L) {
+ val toCopy = minOf(byteCount, s!!.limit - s.pos).toInt()
+ out.write(s.data, s.pos, toCopy)
+
+ s.pos += toCopy
+ size -= toCopy.toLong()
+ byteCount -= toCopy.toLong()
+
+ if (s.pos == s.limit) {
+ val toRecycle = s
+ s = toRecycle.pop()
+ head = s
+ SegmentPool.recycle(toRecycle)
+ }
+ }
+
+ return this
+ }
+
+ /** Read and exhaust bytes from `input` into this. */
+ @Throws(IOException::class)
+ fun readFrom(input: InputStream): Buffer {
+ readFrom(input, Long.MAX_VALUE, true)
+ return this
+ }
+
+ /** Read `byteCount` bytes from `input` into this. */
+ @Throws(IOException::class)
+ fun readFrom(input: InputStream, byteCount: Long): Buffer {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ readFrom(input, byteCount, false)
+ return this
+ }
+
+ @Throws(IOException::class)
+ private fun readFrom(input: InputStream, byteCount: Long, forever: Boolean) {
+ var byteCount = byteCount
+ while (byteCount > 0L || forever) {
+ val tail = writableSegment(1)
+ val maxToCopy = minOf(byteCount, Segment.SIZE - tail.limit).toInt()
+ val bytesRead = input.read(tail.data, tail.limit, maxToCopy)
+ if (bytesRead == -1) {
+ if (tail.pos == tail.limit) {
+ // We allocated a tail segment, but didn't end up needing it. Recycle!
+ head = tail.pop()
+ SegmentPool.recycle(tail)
+ }
+ if (forever) return
+ throw EOFException()
+ }
+ tail.limit += bytesRead
+ size += bytesRead.toLong()
+ byteCount -= bytesRead.toLong()
+ }
+ }
+
+ actual fun completeSegmentByteCount(): Long = commonCompleteSegmentByteCount()
+
+ @Throws(EOFException::class)
+ override fun readByte(): Byte = commonReadByte()
+
+ @JvmName("getByte")
+ actual operator fun get(pos: Long): Byte = commonGet(pos)
+
+ @Throws(EOFException::class)
+ override fun readShort(): Short = commonReadShort()
+
+ @Throws(EOFException::class)
+ override fun readInt(): Int = commonReadInt()
+
+ @Throws(EOFException::class)
+ override fun readLong(): Long = commonReadLong()
+
+ @Throws(EOFException::class)
+ override fun readShortLe() = readShort().reverseBytes()
+
+ @Throws(EOFException::class)
+ override fun readIntLe() = readInt().reverseBytes()
+
+ @Throws(EOFException::class)
+ override fun readLongLe() = readLong().reverseBytes()
+
+ @Throws(EOFException::class)
+ override fun readDecimalLong(): Long = commonReadDecimalLong()
+
+ @Throws(EOFException::class)
+ override fun readHexadecimalUnsignedLong(): Long = commonReadHexadecimalUnsignedLong()
+
+ override fun readByteString(): ByteString = commonReadByteString()
+
+ @Throws(EOFException::class)
+ override fun readByteString(byteCount: Long) = commonReadByteString(byteCount)
+
+ override fun select(options: Options): Int = commonSelect(options)
+
+ @Throws(EOFException::class)
+ override fun readFully(sink: Buffer, byteCount: Long): Unit = commonReadFully(sink, byteCount)
+
+ @Throws(IOException::class)
+ override fun readAll(sink: Sink): Long = commonReadAll(sink)
+
+ override fun readUtf8() = readString(size, Charsets.UTF_8)
+
+ @Throws(EOFException::class)
+ override fun readUtf8(byteCount: Long) = readString(byteCount, Charsets.UTF_8)
+
+ override fun readString(charset: Charset) = readString(size, charset)
+
+ @Throws(EOFException::class)
+ override fun readString(byteCount: Long, charset: Charset): String {
+ require(byteCount >= 0 && byteCount <= Integer.MAX_VALUE) { "byteCount: $byteCount" }
+ if (size < byteCount) throw EOFException()
+ if (byteCount == 0L) return ""
+
+ val s = head!!
+ if (s.pos + byteCount > s.limit) {
+ // If the string spans multiple segments, delegate to readBytes().
+ return String(readByteArray(byteCount), charset)
+ }
+
+ val result = String(s.data, s.pos, byteCount.toInt(), charset)
+ s.pos += byteCount.toInt()
+ size -= byteCount
+
+ if (s.pos == s.limit) {
+ head = s.pop()
+ SegmentPool.recycle(s)
+ }
+
+ return result
+ }
+
+ @Throws(EOFException::class)
+ override fun readUtf8Line(): String? = commonReadUtf8Line()
+
+ @Throws(EOFException::class)
+ override fun readUtf8LineStrict() = readUtf8LineStrict(Long.MAX_VALUE)
+
+ @Throws(EOFException::class)
+ override fun readUtf8LineStrict(limit: Long): String = commonReadUtf8LineStrict(limit)
+
+ @Throws(EOFException::class)
+ override fun readUtf8CodePoint(): Int = commonReadUtf8CodePoint()
+
+ override fun readByteArray() = commonReadByteArray()
+
+ @Throws(EOFException::class)
+ override fun readByteArray(byteCount: Long): ByteArray = commonReadByteArray(byteCount)
+
+ override fun read(sink: ByteArray) = commonRead(sink)
+
+ @Throws(EOFException::class)
+ override fun readFully(sink: ByteArray) = commonReadFully(sink)
+
+ override fun read(sink: ByteArray, offset: Int, byteCount: Int): Int =
+ commonRead(sink, offset, byteCount)
+
+ @Throws(IOException::class)
+ override fun read(sink: ByteBuffer): Int {
+ val s = head ?: return -1
+
+ val toCopy = minOf(sink.remaining(), s.limit - s.pos)
+ sink.put(s.data, s.pos, toCopy)
+
+ s.pos += toCopy
+ size -= toCopy.toLong()
+
+ if (s.pos == s.limit) {
+ head = s.pop()
+ SegmentPool.recycle(s)
+ }
+
+ return toCopy
+ }
+
+ actual fun clear() = commonClear()
+
+ @Throws(EOFException::class)
+ actual override fun skip(byteCount: Long) = commonSkip(byteCount)
+
+ actual override fun write(byteString: ByteString): Buffer = commonWrite(byteString)
+
+ actual override fun write(byteString: ByteString, offset: Int, byteCount: Int) =
+ commonWrite(byteString, offset, byteCount)
+
+ actual override fun writeUtf8(string: String): Buffer = writeUtf8(string, 0, string.length)
+
+ actual override fun writeUtf8(string: String, beginIndex: Int, endIndex: Int): Buffer =
+ commonWriteUtf8(string, beginIndex, endIndex)
+
+ actual override fun writeUtf8CodePoint(codePoint: Int): Buffer =
+ commonWriteUtf8CodePoint(codePoint)
+
+ override fun writeString(string: String, charset: Charset) = writeString(
+ string, 0, string.length,
+ charset
+ )
+
+ override fun writeString(
+ string: String,
+ beginIndex: Int,
+ endIndex: Int,
+ charset: Charset
+ ): Buffer {
+ require(beginIndex >= 0) { "beginIndex < 0: $beginIndex" }
+ require(endIndex >= beginIndex) { "endIndex < beginIndex: $endIndex < $beginIndex" }
+ require(endIndex <= string.length) { "endIndex > string.length: $endIndex > ${string.length}" }
+ if (charset == Charsets.UTF_8) return writeUtf8(string, beginIndex, endIndex)
+ val data = string.substring(beginIndex, endIndex).toByteArray(charset)
+ return write(data, 0, data.size)
+ }
+
+ actual override fun write(source: ByteArray): Buffer = commonWrite(source)
+
+ actual override fun write(
+ source: ByteArray,
+ offset: Int,
+ byteCount: Int
+ ): Buffer = commonWrite(source, offset, byteCount)
+
+ @Throws(IOException::class)
+ override fun write(source: ByteBuffer): Int {
+ val byteCount = source.remaining()
+ var remaining = byteCount
+ while (remaining > 0) {
+ val tail = writableSegment(1)
+
+ val toCopy = minOf(remaining, Segment.SIZE - tail.limit)
+ source.get(tail.data, tail.limit, toCopy)
+
+ remaining -= toCopy
+ tail.limit += toCopy
+ }
+
+ size += byteCount.toLong()
+ return byteCount
+ }
+
+ @Throws(IOException::class)
+ override fun writeAll(source: Source): Long = commonWriteAll(source)
+
+ @Throws(IOException::class)
+ actual override fun write(source: Source, byteCount: Long): Buffer =
+ commonWrite(source, byteCount)
+
+ actual override fun writeByte(b: Int): Buffer = commonWriteByte(b)
+
+ actual override fun writeShort(s: Int): Buffer = commonWriteShort(s)
+
+ actual override fun writeShortLe(s: Int) = writeShort(s.toShort().reverseBytes().toInt())
+
+ actual override fun writeInt(i: Int): Buffer = commonWriteInt(i)
+
+ actual override fun writeIntLe(i: Int) = writeInt(i.reverseBytes())
+
+ actual override fun writeLong(v: Long): Buffer = commonWriteLong(v)
+
+ actual override fun writeLongLe(v: Long) = writeLong(v.reverseBytes())
+
+ actual override fun writeDecimalLong(v: Long): Buffer = commonWriteDecimalLong(v)
+
+ actual override fun writeHexadecimalUnsignedLong(v: Long): Buffer =
+ commonWriteHexadecimalUnsignedLong(v)
+
+ internal actual fun writableSegment(minimumCapacity: Int): Segment =
+ commonWritableSegment(minimumCapacity)
+
+ override fun write(source: Buffer, byteCount: Long): Unit = commonWrite(source, byteCount)
+
+ override fun read(sink: Buffer, byteCount: Long): Long = commonRead(sink, byteCount)
+
+ override fun indexOf(b: Byte) = indexOf(b, 0, Long.MAX_VALUE)
+
+ /**
+ * Returns the index of `b` in this at or beyond `fromIndex`, or -1 if this buffer does not
+ * contain `b` in that range.
+ */
+ override fun indexOf(b: Byte, fromIndex: Long) = indexOf(b, fromIndex, Long.MAX_VALUE)
+
+ override fun indexOf(b: Byte, fromIndex: Long, toIndex: Long): Long = commonIndexOf(b, fromIndex, toIndex)
+
+ @Throws(IOException::class)
+ override fun indexOf(bytes: ByteString): Long = indexOf(bytes, 0)
+
+ @Throws(IOException::class)
+ override fun indexOf(bytes: ByteString, fromIndex: Long): Long = commonIndexOf(bytes, fromIndex)
+
+ override fun indexOfElement(targetBytes: ByteString) = indexOfElement(targetBytes, 0L)
+
+ override fun indexOfElement(targetBytes: ByteString, fromIndex: Long): Long =
+ commonIndexOfElement(targetBytes, fromIndex)
+
+ override fun rangeEquals(offset: Long, bytes: ByteString) =
+ rangeEquals(offset, bytes, 0, bytes.size)
+
+ override fun rangeEquals(
+ offset: Long,
+ bytes: ByteString,
+ bytesOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, bytes, bytesOffset, byteCount)
+
+ override fun flush() {}
+
+ override fun isOpen() = true
+
+ override fun close() {}
+
+ override fun timeout() = Timeout.NONE
+
+ /** Returns the 128-bit MD5 hash of this buffer. */
+ actual fun md5() = digest("MD5")
+
+ /** Returns the 160-bit SHA-1 hash of this buffer. */
+ actual fun sha1() = digest("SHA-1")
+
+ /** Returns the 256-bit SHA-256 hash of this buffer. */
+ actual fun sha256() = digest("SHA-256")
+
+ /** Returns the 512-bit SHA-512 hash of this buffer. */
+ actual fun sha512() = digest("SHA-512")
+
+ private fun digest(algorithm: String): ByteString {
+ val messageDigest = MessageDigest.getInstance(algorithm)
+ head?.let { head ->
+ messageDigest.update(head.data, head.pos, head.limit - head.pos)
+ var s = head.next!!
+ while (s !== head) {
+ messageDigest.update(s.data, s.pos, s.limit - s.pos)
+ s = s.next!!
+ }
+ }
+ return ByteString(messageDigest.digest())
+ }
+
+ /** Returns the 160-bit SHA-1 HMAC of this buffer. */
+ actual fun hmacSha1(key: ByteString) = hmac("HmacSHA1", key)
+
+ /** Returns the 256-bit SHA-256 HMAC of this buffer. */
+ actual fun hmacSha256(key: ByteString) = hmac("HmacSHA256", key)
+
+ /** Returns the 512-bit SHA-512 HMAC of this buffer. */
+ actual fun hmacSha512(key: ByteString) = hmac("HmacSHA512", key)
+
+ private fun hmac(algorithm: String, key: ByteString): ByteString {
+ try {
+ val mac = Mac.getInstance(algorithm)
+ mac.init(SecretKeySpec(key.internalArray(), algorithm))
+ head?.let { head ->
+ mac.update(head.data, head.pos, head.limit - head.pos)
+ var s = head.next!!
+ while (s !== head) {
+ mac.update(s.data, s.pos, s.limit - s.pos)
+ s = s.next!!
+ }
+ }
+ return ByteString(mac.doFinal())
+ } catch (e: InvalidKeyException) {
+ throw IllegalArgumentException(e)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean = commonEquals(other)
+
+ override fun hashCode(): Int = commonHashCode()
+
+ /**
+ * Returns a human-readable string that describes the contents of this buffer. Typically this
+ * is a string like `[text=Hello]` or `[hex=0000ffff]`.
+ */
+ override fun toString() = snapshot().toString()
+
+ actual fun copy(): Buffer = commonCopy()
+
+ /** Returns a deep copy of this buffer. */
+ public override fun clone(): Buffer = copy()
+
+ actual fun snapshot(): ByteString = commonSnapshot()
+
+ actual fun snapshot(byteCount: Int): ByteString = commonSnapshot(byteCount)
+
+ @JvmOverloads
+ actual fun readUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor = commonReadUnsafe(unsafeCursor)
+
+ @JvmOverloads
+ actual fun readAndWriteUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor =
+ commonReadAndWriteUnsafe(unsafeCursor)
+
+ @JvmName("-deprecated_getByte")
+ @Deprecated(
+ message = "moved to operator function",
+ replaceWith = ReplaceWith(expression = "this[index]"),
+ level = DeprecationLevel.ERROR
+ )
+ fun getByte(index: Long) = this[index]
+
+ @JvmName("-deprecated_size")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "size"),
+ level = DeprecationLevel.ERROR
+ )
+ fun size() = size
+
+ actual class UnsafeCursor : Closeable {
+ @JvmField actual var buffer: Buffer? = null
+ @JvmField actual var readWrite: Boolean = false
+
+ internal actual var segment: Segment? = null
+ @JvmField actual var offset = -1L
+ @JvmField actual var data: ByteArray? = null
+ @JvmField actual var start = -1
+ @JvmField actual var end = -1
+
+ actual fun next(): Int = commonNext()
+
+ actual fun seek(offset: Long): Int = commonSeek(offset)
+
+ actual fun resizeBuffer(newSize: Long): Long = commonResizeBuffer(newSize)
+
+ actual fun expandBuffer(minByteCount: Int): Long = commonExpandBuffer(minByteCount)
+
+ actual override fun close() {
+ commonClose()
+ }
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/BufferedSink.kt b/okio/src/jvmMain/kotlin/okio/BufferedSink.kt
new file mode 100644
index 00000000..4aa1bb06
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/BufferedSink.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import java.io.OutputStream
+import java.nio.channels.WritableByteChannel
+import java.nio.charset.Charset
+
+actual interface BufferedSink : Sink, WritableByteChannel {
+ /** Returns this sink's internal buffer. */
+ @Deprecated(
+ message = "moved to val: use getBuffer() instead",
+ replaceWith = ReplaceWith(expression = "buffer"),
+ level = DeprecationLevel.WARNING
+ )
+ fun buffer(): Buffer
+
+ actual val buffer: Buffer
+
+ @Throws(IOException::class)
+ actual fun write(byteString: ByteString): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun write(byteString: ByteString, offset: Int, byteCount: Int): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun write(source: ByteArray): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun write(source: ByteArray, offset: Int, byteCount: Int): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeAll(source: Source): Long
+
+ @Throws(IOException::class)
+ actual fun write(source: Source, byteCount: Long): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeUtf8(string: String): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeUtf8(string: String, beginIndex: Int, endIndex: Int): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeUtf8CodePoint(codePoint: Int): BufferedSink
+
+ @Throws(IOException::class)
+ fun writeString(string: String, charset: Charset): BufferedSink
+
+ @Throws(IOException::class)
+ fun writeString(string: String, beginIndex: Int, endIndex: Int, charset: Charset): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeByte(b: Int): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeShort(s: Int): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeShortLe(s: Int): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeInt(i: Int): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeIntLe(i: Int): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeLong(v: Long): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeLongLe(v: Long): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeDecimalLong(v: Long): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun writeHexadecimalUnsignedLong(v: Long): BufferedSink
+
+ @Throws(IOException::class)
+ actual override fun flush()
+
+ @Throws(IOException::class)
+ actual fun emit(): BufferedSink
+
+ @Throws(IOException::class)
+ actual fun emitCompleteSegments(): BufferedSink
+
+ /** Returns an output stream that writes to this sink. */
+ fun outputStream(): OutputStream
+}
diff --git a/okio/src/jvmMain/kotlin/okio/BufferedSource.kt b/okio/src/jvmMain/kotlin/okio/BufferedSource.kt
new file mode 100644
index 00000000..b312d569
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/BufferedSource.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import java.io.InputStream
+import java.nio.channels.ReadableByteChannel
+import java.nio.charset.Charset
+
+actual interface BufferedSource : Source, ReadableByteChannel {
+ /** Returns this source's internal buffer. */
+ @Deprecated(
+ message = "moved to val: use getBuffer() instead",
+ replaceWith = ReplaceWith(expression = "buffer"),
+ level = DeprecationLevel.WARNING
+ )
+ fun buffer(): Buffer
+
+ actual val buffer: Buffer
+
+ @Throws(IOException::class)
+ actual fun exhausted(): Boolean
+
+ @Throws(IOException::class)
+ actual fun require(byteCount: Long)
+
+ @Throws(IOException::class)
+ actual fun request(byteCount: Long): Boolean
+
+ @Throws(IOException::class)
+ actual fun readByte(): Byte
+
+ @Throws(IOException::class)
+ actual fun readShort(): Short
+
+ @Throws(IOException::class)
+ actual fun readShortLe(): Short
+
+ @Throws(IOException::class)
+ actual fun readInt(): Int
+
+ @Throws(IOException::class)
+ actual fun readIntLe(): Int
+
+ @Throws(IOException::class)
+ actual fun readLong(): Long
+
+ @Throws(IOException::class)
+ actual fun readLongLe(): Long
+
+ @Throws(IOException::class)
+ actual fun readDecimalLong(): Long
+
+ @Throws(IOException::class)
+ actual fun readHexadecimalUnsignedLong(): Long
+
+ @Throws(IOException::class)
+ actual fun skip(byteCount: Long)
+
+ @Throws(IOException::class)
+ actual fun readByteString(): ByteString
+
+ @Throws(IOException::class)
+ actual fun readByteString(byteCount: Long): ByteString
+
+ @Throws(IOException::class)
+ actual fun select(options: Options): Int
+
+ @Throws(IOException::class)
+ actual fun readByteArray(): ByteArray
+
+ @Throws(IOException::class)
+ actual fun readByteArray(byteCount: Long): ByteArray
+
+ @Throws(IOException::class)
+ actual fun read(sink: ByteArray): Int
+
+ @Throws(IOException::class)
+ actual fun readFully(sink: ByteArray)
+
+ @Throws(IOException::class)
+ actual fun read(sink: ByteArray, offset: Int, byteCount: Int): Int
+
+ @Throws(IOException::class)
+ actual fun readFully(sink: Buffer, byteCount: Long)
+
+ @Throws(IOException::class)
+ actual fun readAll(sink: Sink): Long
+
+ @Throws(IOException::class)
+ actual fun readUtf8(): String
+
+ @Throws(IOException::class)
+ actual fun readUtf8(byteCount: Long): String
+
+ @Throws(IOException::class)
+ actual fun readUtf8Line(): String?
+
+ @Throws(IOException::class)
+ actual fun readUtf8LineStrict(): String
+
+ @Throws(IOException::class)
+ actual fun readUtf8LineStrict(limit: Long): String
+
+ @Throws(IOException::class)
+ actual fun readUtf8CodePoint(): Int
+
+ /** Removes all bytes from this, decodes them as `charset`, and returns the string. */
+ @Throws(IOException::class)
+ fun readString(charset: Charset): String
+
+ /**
+ * Removes `byteCount` bytes from this, decodes them as `charset`, and returns the
+ * string.
+ */
+ @Throws(IOException::class)
+ fun readString(byteCount: Long, charset: Charset): String
+
+ @Throws(IOException::class)
+ actual fun indexOf(b: Byte): Long
+
+ @Throws(IOException::class)
+ actual fun indexOf(b: Byte, fromIndex: Long): Long
+
+ @Throws(IOException::class)
+ actual fun indexOf(b: Byte, fromIndex: Long, toIndex: Long): Long
+
+ @Throws(IOException::class)
+ actual fun indexOf(bytes: ByteString): Long
+
+ @Throws(IOException::class)
+ actual fun indexOf(bytes: ByteString, fromIndex: Long): Long
+
+ @Throws(IOException::class)
+ actual fun indexOfElement(targetBytes: ByteString): Long
+
+ @Throws(IOException::class)
+ actual fun indexOfElement(targetBytes: ByteString, fromIndex: Long): Long
+
+ @Throws(IOException::class)
+ actual fun rangeEquals(offset: Long, bytes: ByteString): Boolean
+
+ @Throws(IOException::class)
+ actual fun rangeEquals(offset: Long, bytes: ByteString, bytesOffset: Int, byteCount: Int): Boolean
+
+ actual fun peek(): BufferedSource
+
+ /** Returns an input stream that reads from this source. */
+ fun inputStream(): InputStream
+}
diff --git a/okio/src/jvmMain/kotlin/okio/ByteString.kt b/okio/src/jvmMain/kotlin/okio/ByteString.kt
new file mode 100644
index 00000000..0edad9cb
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/ByteString.kt
@@ -0,0 +1,354 @@
+/*
+ * Copyright 2014 Square Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.commonBase64
+import okio.internal.commonBase64Url
+import okio.internal.commonCompareTo
+import okio.internal.commonDecodeBase64
+import okio.internal.commonDecodeHex
+import okio.internal.commonEncodeUtf8
+import okio.internal.commonEndsWith
+import okio.internal.commonEquals
+import okio.internal.commonGetByte
+import okio.internal.commonGetSize
+import okio.internal.commonHashCode
+import okio.internal.commonHex
+import okio.internal.commonIndexOf
+import okio.internal.commonInternalArray
+import okio.internal.commonLastIndexOf
+import okio.internal.commonOf
+import okio.internal.commonRangeEquals
+import okio.internal.commonStartsWith
+import okio.internal.commonSubstring
+import okio.internal.commonToAsciiLowercase
+import okio.internal.commonToAsciiUppercase
+import okio.internal.commonToByteArray
+import okio.internal.commonToByteString
+import okio.internal.commonToString
+import okio.internal.commonUtf8
+import okio.internal.commonWrite
+import java.io.EOFException
+import java.io.IOException
+import java.io.InputStream
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.io.OutputStream
+import java.io.Serializable
+import java.nio.ByteBuffer
+import java.nio.charset.Charset
+import java.security.InvalidKeyException
+import java.security.MessageDigest
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+
+actual open class ByteString
+internal actual constructor(
+ internal actual val data: ByteArray
+) : Serializable, Comparable<ByteString> {
+ @Transient internal actual var hashCode: Int = 0 // Lazily computed; 0 if unknown.
+ @Transient internal actual var utf8: String? = null // Lazily computed.
+
+ actual open fun utf8(): String = commonUtf8()
+
+ /** Constructs a new `String` by decoding the bytes using `charset`. */
+ open fun string(charset: Charset) = String(data, charset)
+
+ actual open fun base64() = commonBase64()
+
+ actual fun md5() = digest("MD5")
+
+ actual fun sha1() = digest("SHA-1")
+
+ actual fun sha256() = digest("SHA-256")
+
+ actual fun sha512() = digest("SHA-512")
+
+ internal open fun digest(algorithm: String): ByteString {
+ val digestBytes = MessageDigest.getInstance(algorithm).run {
+ update(data, 0, size)
+ digest()
+ }
+ return ByteString(digestBytes)
+ }
+
+ /** Returns the 160-bit SHA-1 HMAC of this byte string. */
+ actual open fun hmacSha1(key: ByteString) = hmac("HmacSHA1", key)
+
+ /** Returns the 256-bit SHA-256 HMAC of this byte string. */
+ actual open fun hmacSha256(key: ByteString) = hmac("HmacSHA256", key)
+
+ /** Returns the 512-bit SHA-512 HMAC of this byte string. */
+ actual open fun hmacSha512(key: ByteString) = hmac("HmacSHA512", key)
+
+ internal open fun hmac(algorithm: String, key: ByteString): ByteString {
+ try {
+ val mac = Mac.getInstance(algorithm)
+ mac.init(SecretKeySpec(key.toByteArray(), algorithm))
+ return ByteString(mac.doFinal(data))
+ } catch (e: InvalidKeyException) {
+ throw IllegalArgumentException(e)
+ }
+ }
+
+ actual open fun base64Url() = commonBase64Url()
+
+ actual open fun hex(): String = commonHex()
+
+ actual open fun toAsciiLowercase(): ByteString = commonToAsciiLowercase()
+
+ actual open fun toAsciiUppercase(): ByteString = commonToAsciiUppercase()
+
+ @JvmOverloads
+ actual open fun substring(beginIndex: Int, endIndex: Int): ByteString =
+ commonSubstring(beginIndex, endIndex)
+
+ internal actual open fun internalGet(pos: Int) = commonGetByte(pos)
+
+ @JvmName("getByte")
+ actual operator fun get(index: Int): Byte = internalGet(index)
+
+ actual val size
+ @JvmName("size") get() = getSize()
+
+ internal actual open fun getSize() = commonGetSize()
+
+ actual open fun toByteArray() = commonToByteArray()
+
+ internal actual open fun internalArray() = commonInternalArray()
+
+ /** Returns a `ByteBuffer` view of the bytes in this `ByteString`. */
+ open fun asByteBuffer(): ByteBuffer = ByteBuffer.wrap(data).asReadOnlyBuffer()
+
+ /** Writes the contents of this byte string to `out`. */
+ @Throws(IOException::class)
+ open fun write(out: OutputStream) {
+ out.write(data)
+ }
+
+ internal actual open fun write(buffer: Buffer, offset: Int, byteCount: Int) =
+ commonWrite(buffer, offset, byteCount)
+
+ actual open fun rangeEquals(
+ offset: Int,
+ other: ByteString,
+ otherOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount)
+
+ actual open fun rangeEquals(
+ offset: Int,
+ other: ByteArray,
+ otherOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount)
+
+ actual fun startsWith(prefix: ByteString) = commonStartsWith(prefix)
+
+ actual fun startsWith(prefix: ByteArray) = commonStartsWith(prefix)
+
+ actual fun endsWith(suffix: ByteString) = commonEndsWith(suffix)
+
+ actual fun endsWith(suffix: ByteArray) = commonEndsWith(suffix)
+
+ @JvmOverloads
+ actual fun indexOf(other: ByteString, fromIndex: Int) = indexOf(other.internalArray(), fromIndex)
+
+ @JvmOverloads
+ actual open fun indexOf(other: ByteArray, fromIndex: Int) = commonIndexOf(other, fromIndex)
+
+ @JvmOverloads
+ actual fun lastIndexOf(other: ByteString, fromIndex: Int) = commonLastIndexOf(other, fromIndex)
+
+ @JvmOverloads
+ actual open fun lastIndexOf(other: ByteArray, fromIndex: Int) = commonLastIndexOf(other, fromIndex)
+
+ actual override fun equals(other: Any?) = commonEquals(other)
+
+ actual override fun hashCode() = commonHashCode()
+
+ actual override fun compareTo(other: ByteString) = commonCompareTo(other)
+
+ actual override fun toString() = commonToString()
+
+ @Throws(IOException::class)
+ private fun readObject(`in`: ObjectInputStream) {
+ val dataLength = `in`.readInt()
+ val byteString = `in`.readByteString(dataLength)
+ val field = ByteString::class.java.getDeclaredField("data")
+ field.isAccessible = true
+ field.set(this, byteString.data)
+ }
+
+ @Throws(IOException::class)
+ private fun writeObject(out: ObjectOutputStream) {
+ out.writeInt(data.size)
+ out.write(data)
+ }
+
+ @JvmName("-deprecated_getByte")
+ @Deprecated(
+ message = "moved to operator function",
+ replaceWith = ReplaceWith(expression = "this[index]"),
+ level = DeprecationLevel.ERROR
+ )
+ fun getByte(index: Int) = this[index]
+
+ @JvmName("-deprecated_size")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "size"),
+ level = DeprecationLevel.ERROR
+ )
+ fun size() = size
+
+ actual companion object {
+ private const val serialVersionUID = 1L
+
+ @JvmField
+ actual val EMPTY: ByteString = ByteString(byteArrayOf())
+
+ @JvmStatic
+ actual fun of(vararg data: Byte) = commonOf(data)
+
+ @JvmStatic
+ @JvmName("of")
+ actual fun ByteArray.toByteString(offset: Int, byteCount: Int): ByteString =
+ commonToByteString(offset, byteCount)
+
+ /** Returns a [ByteString] containing a copy of this [ByteBuffer]. */
+ @JvmStatic
+ @JvmName("of")
+ fun ByteBuffer.toByteString(): ByteString {
+ val copy = ByteArray(remaining())
+ get(copy)
+ return ByteString(copy)
+ }
+
+ @JvmStatic
+ actual fun String.encodeUtf8(): ByteString = commonEncodeUtf8()
+
+ /** Returns a new [ByteString] containing the `charset`-encoded bytes of this [String]. */
+ @JvmStatic
+ @JvmName("encodeString")
+ fun String.encode(charset: Charset = Charsets.UTF_8) = ByteString(toByteArray(charset))
+
+ @JvmStatic
+ actual fun String.decodeBase64() = commonDecodeBase64()
+
+ @JvmStatic
+ actual fun String.decodeHex() = commonDecodeHex()
+
+ /**
+ * Reads `count` bytes from this [InputStream] and returns the result.
+ *
+ * @throws java.io.EOFException if `in` has fewer than `count` bytes to read.
+ */
+ @Throws(IOException::class)
+ @JvmStatic
+ @JvmName("read")
+ fun InputStream.readByteString(byteCount: Int): ByteString {
+ require(byteCount >= 0) { "byteCount < 0: $byteCount" }
+
+ val result = ByteArray(byteCount)
+ var offset = 0
+ var read: Int
+ while (offset < byteCount) {
+ read = read(result, offset, byteCount - offset)
+ if (read == -1) throw EOFException()
+ offset += read
+ }
+ return ByteString(result)
+ }
+
+ @JvmName("-deprecated_decodeBase64")
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "string.decodeBase64()",
+ imports = ["okio.ByteString.Companion.decodeBase64"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun decodeBase64(string: String) = string.decodeBase64()
+
+ @JvmName("-deprecated_decodeHex")
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "string.decodeHex()",
+ imports = ["okio.ByteString.Companion.decodeHex"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun decodeHex(string: String) = string.decodeHex()
+
+ @JvmName("-deprecated_encodeString")
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "string.encode(charset)",
+ imports = ["okio.ByteString.Companion.encode"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun encodeString(string: String, charset: Charset) = string.encode(charset)
+
+ @JvmName("-deprecated_encodeUtf8")
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "string.encodeUtf8()",
+ imports = ["okio.ByteString.Companion.encodeUtf8"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun encodeUtf8(string: String) = string.encodeUtf8()
+
+ @JvmName("-deprecated_of")
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "buffer.toByteString()",
+ imports = ["okio.ByteString.Companion.toByteString"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun of(buffer: ByteBuffer) = buffer.toByteString()
+
+ @JvmName("-deprecated_of")
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "array.toByteString(offset, byteCount)",
+ imports = ["okio.ByteString.Companion.toByteString"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun of(array: ByteArray, offset: Int, byteCount: Int) = array.toByteString(offset, byteCount)
+
+ @JvmName("-deprecated_read")
+ @Deprecated(
+ message = "moved to extension function",
+ replaceWith = ReplaceWith(
+ expression = "inputstream.readByteString(byteCount)",
+ imports = ["okio.ByteString.Companion.readByteString"]
+ ),
+ level = DeprecationLevel.ERROR
+ )
+ fun read(inputstream: InputStream, byteCount: Int) = inputstream.readByteString(byteCount)
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/CipherSink.kt b/okio/src/jvmMain/kotlin/okio/CipherSink.kt
new file mode 100644
index 00000000..aa482842
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/CipherSink.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import javax.crypto.Cipher
+
+class CipherSink(
+ private val sink: BufferedSink,
+ val cipher: Cipher
+) : Sink {
+ private val blockSize = cipher.blockSize
+ private var closed = false
+
+ init {
+ // Require block cipher
+ require(blockSize > 0) { "Block cipher required $cipher" }
+ }
+
+ @Throws(IOException::class)
+ override fun write(source: Buffer, byteCount: Long) {
+ checkOffsetAndCount(source.size, 0, byteCount)
+ check(!closed) { "closed" }
+
+ var remaining = byteCount
+ while (remaining > 0) {
+ val size = update(source, remaining)
+ remaining -= size
+ }
+ }
+
+ private fun update(source: Buffer, remaining: Long): Int {
+ val head = source.head!!
+ var size = minOf(remaining, head.limit - head.pos).toInt()
+ val buffer = sink.buffer
+
+ // Shorten input until output is guaranteed to fit within a segment
+ var outputSize = cipher.getOutputSize(size)
+ while (outputSize > Segment.SIZE) {
+ check(size > blockSize) { "Unexpected output size $outputSize for input size $size" }
+ size -= blockSize
+ outputSize = cipher.getOutputSize(size)
+ }
+ val s = buffer.writableSegment(outputSize)
+
+ val ciphered = cipher.update(head.data, head.pos, size, s.data, s.limit)
+
+ s.limit += ciphered
+ buffer.size += ciphered
+
+ // We allocated a tail segment, but didn't end up needing it. Recycle!
+ if (s.pos == s.limit) {
+ buffer.head = s.pop()
+ SegmentPool.recycle(s)
+ }
+
+ sink.emitCompleteSegments()
+
+ // Mark those bytes as read.
+ source.size -= size
+ head.pos += size
+ if (head.pos == head.limit) {
+ source.head = head.pop()
+ SegmentPool.recycle(head)
+ }
+
+ return size
+ }
+
+ override fun flush() = sink.flush()
+
+ override fun timeout() = sink.timeout()
+
+ @Throws(IOException::class)
+ override fun close() {
+ if (closed) return
+ closed = true
+
+ var thrown = doFinal()
+
+ try {
+ sink.close()
+ } catch (e: Throwable) {
+ if (thrown == null) thrown = e
+ }
+
+ if (thrown != null) throw thrown
+ }
+
+ private fun doFinal(): Throwable? {
+ val outputSize = cipher.getOutputSize(0)
+ if (outputSize == 0) return null
+
+ var thrown: Throwable? = null
+ val buffer = sink.buffer
+
+ // For block cipher, output size cannot exceed block size in doFinal
+ val s = buffer.writableSegment(outputSize)
+
+ try {
+ val ciphered = cipher.doFinal(s.data, s.limit)
+
+ s.limit += ciphered
+ buffer.size += ciphered
+ } catch (e: Throwable) {
+ thrown = e
+ }
+
+ if (s.pos == s.limit) {
+ buffer.head = s.pop()
+ SegmentPool.recycle(s)
+ }
+
+ return thrown
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/CipherSource.kt b/okio/src/jvmMain/kotlin/okio/CipherSource.kt
new file mode 100644
index 00000000..154371f3
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/CipherSource.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import javax.crypto.Cipher
+
+class CipherSource(
+ private val source: BufferedSource,
+ val cipher: Cipher
+) : Source {
+ private val blockSize = cipher.blockSize
+ private val buffer = Buffer()
+ private var final = false
+ private var closed = false
+
+ init {
+ // Require block cipher
+ require(blockSize > 0) { "Block cipher required $cipher" }
+ }
+
+ @Throws(IOException::class)
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ check(!closed) { "closed" }
+ if (byteCount == 0L) return 0L
+ if (final) return buffer.read(sink, byteCount)
+
+ refill()
+
+ return buffer.read(sink, byteCount)
+ }
+
+ private fun refill() {
+ while (buffer.size == 0L) {
+ if (source.exhausted()) {
+ final = true
+ doFinal()
+ break
+ } else {
+ update()
+ }
+ }
+ }
+
+ private fun update() {
+ val head = source.buffer.head!!
+ var size = head.limit - head.pos
+
+ // Shorten input until output is guaranteed to fit within a segment
+ var outputSize = cipher.getOutputSize(size)
+ while (outputSize > Segment.SIZE) {
+ check(size > blockSize) { "Unexpected output size $outputSize for input size $size" }
+ size -= blockSize
+ outputSize = cipher.getOutputSize(size)
+ }
+ val s = buffer.writableSegment(outputSize)
+
+ val ciphered =
+ cipher.update(head.data, head.pos, size, s.data, s.pos)
+
+ source.skip(size.toLong())
+
+ s.limit += ciphered
+ buffer.size += ciphered
+
+ // We allocated a tail segment, but didn't end up needing it. Recycle!
+ if (s.pos == s.limit) {
+ buffer.head = s.pop()
+ SegmentPool.recycle(s)
+ }
+ }
+
+ private fun doFinal() {
+ val outputSize = cipher.getOutputSize(0)
+ if (outputSize == 0) return
+
+ // For block cipher, output size cannot exceed block size in doFinal.
+ val s = buffer.writableSegment(outputSize)
+
+ val ciphered = cipher.doFinal(s.data, s.pos)
+
+ s.limit += ciphered
+ buffer.size += ciphered
+
+ // We allocated a tail segment, but didn't end up needing it. Recycle!
+ if (s.pos == s.limit) {
+ buffer.head = s.pop()
+ SegmentPool.recycle(s)
+ }
+ }
+
+ override fun timeout() = source.timeout()
+
+ @Throws(IOException::class)
+ override fun close() {
+ closed = true
+ source.close()
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/DeflaterSink.kt b/okio/src/jvmMain/kotlin/okio/DeflaterSink.kt
new file mode 100644
index 00000000..e71cdff8
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/DeflaterSink.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("-DeflaterSinkExtensions")
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package okio
+
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+import java.io.IOException
+import java.util.zip.Deflater
+
+/**
+ * A sink that uses [DEFLATE](http://tools.ietf.org/html/rfc1951) to
+ * compress data written to another source.
+ *
+ * ### Sync flush
+ *
+ * Aggressive flushing of this stream may result in reduced compression. Each
+ * call to [flush] immediately compresses all currently-buffered data;
+ * this early compression may be less effective than compression performed
+ * without flushing.
+ *
+ * This is equivalent to using [Deflater] with the sync flush option.
+ * This class does not offer any partial flush mechanism. For best performance,
+ * only call [flush] when application behavior requires it.
+ */
+class DeflaterSink
+/**
+ * This internal constructor shares a buffer with its trusted caller.
+ * In general we can't share a BufferedSource because the deflater holds input
+ * bytes until they are inflated.
+ */
+internal constructor(private val sink: BufferedSink, private val deflater: Deflater) : Sink {
+ constructor(sink: Sink, deflater: Deflater) : this(sink.buffer(), deflater)
+
+ private var closed = false
+
+ @Throws(IOException::class)
+ override fun write(source: Buffer, byteCount: Long) {
+ checkOffsetAndCount(source.size, 0, byteCount)
+
+ var remaining = byteCount
+ while (remaining > 0) {
+ // Share bytes from the head segment of 'source' with the deflater.
+ val head = source.head!!
+ val toDeflate = minOf(remaining, head.limit - head.pos).toInt()
+ deflater.setInput(head.data, head.pos, toDeflate)
+
+ // Deflate those bytes into sink.
+ deflate(false)
+
+ // Mark those bytes as read.
+ source.size -= toDeflate
+ head.pos += toDeflate
+ if (head.pos == head.limit) {
+ source.head = head.pop()
+ SegmentPool.recycle(head)
+ }
+
+ remaining -= toDeflate
+ }
+ }
+
+ @IgnoreJRERequirement
+ private fun deflate(syncFlush: Boolean) {
+ val buffer = sink.buffer
+ while (true) {
+ val s = buffer.writableSegment(1)
+
+ // The 4-parameter overload of deflate() doesn't exist in the RI until
+ // Java 1.7, and is public (although with @hide) on Android since 2.3.
+ // The @hide tag means that this code won't compile against the Android
+ // 2.3 SDK, but it will run fine there.
+ val deflated = if (syncFlush) {
+ deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit, Deflater.SYNC_FLUSH)
+ } else {
+ deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit)
+ }
+
+ if (deflated > 0) {
+ s.limit += deflated
+ buffer.size += deflated
+ sink.emitCompleteSegments()
+ } else if (deflater.needsInput()) {
+ if (s.pos == s.limit) {
+ // We allocated a tail segment, but didn't end up needing it. Recycle!
+ buffer.head = s.pop()
+ SegmentPool.recycle(s)
+ }
+ return
+ }
+ }
+ }
+
+ @Throws(IOException::class)
+ override fun flush() {
+ deflate(true)
+ sink.flush()
+ }
+
+ internal fun finishDeflate() {
+ deflater.finish()
+ deflate(false)
+ }
+
+ @Throws(IOException::class)
+ override fun close() {
+ if (closed) return
+
+ // Emit deflated data to the underlying sink. If this fails, we still need
+ // to close the deflater and the sink; otherwise we risk leaking resources.
+ var thrown: Throwable? = null
+ try {
+ finishDeflate()
+ } catch (e: Throwable) {
+ thrown = e
+ }
+
+ try {
+ deflater.end()
+ } catch (e: Throwable) {
+ if (thrown == null) thrown = e
+ }
+
+ try {
+ sink.close()
+ } catch (e: Throwable) {
+ if (thrown == null) thrown = e
+ }
+
+ closed = true
+
+ if (thrown != null) throw thrown
+ }
+
+ override fun timeout(): Timeout = sink.timeout()
+
+ override fun toString() = "DeflaterSink($sink)"
+}
+
+/**
+ * Returns an [DeflaterSink] that DEFLATE-compresses data to this [Sink] while writing.
+ *
+ * @see DeflaterSink
+ */
+inline fun Sink.deflate(deflater: Deflater = Deflater()): DeflaterSink =
+ DeflaterSink(this, deflater)
diff --git a/okio/src/jvmMain/kotlin/okio/ForwardingSink.kt b/okio/src/jvmMain/kotlin/okio/ForwardingSink.kt
new file mode 100644
index 00000000..8f0eb2f1
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/ForwardingSink.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+
+/** A [Sink] which forwards calls to another. Useful for subclassing. */
+abstract class ForwardingSink(
+ /** [Sink] to which this instance is delegating. */
+ @get:JvmName("delegate")
+ val delegate: Sink
+) : Sink {
+ // TODO 'Sink by delegate' once https://youtrack.jetbrains.com/issue/KT-23935 is fixed.
+
+ @Throws(IOException::class)
+ override fun write(source: Buffer, byteCount: Long) = delegate.write(source, byteCount)
+
+ @Throws(IOException::class)
+ override fun flush() = delegate.flush()
+
+ override fun timeout() = delegate.timeout()
+
+ @Throws(IOException::class)
+ override fun close() = delegate.close()
+
+ override fun toString() = "${javaClass.simpleName}($delegate)"
+
+ @JvmName("-deprecated_delegate")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "delegate"),
+ level = DeprecationLevel.ERROR
+ )
+ fun delegate() = delegate
+}
diff --git a/okio/src/jvmMain/kotlin/okio/ForwardingSource.kt b/okio/src/jvmMain/kotlin/okio/ForwardingSource.kt
new file mode 100644
index 00000000..30a47f6c
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/ForwardingSource.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+
+/** A [Source] which forwards calls to another. Useful for subclassing. */
+abstract class ForwardingSource(
+ /** [Source] to which this instance is delegating. */
+ @get:JvmName("delegate")
+ val delegate: Source
+) : Source {
+ // TODO 'Source by delegate' once https://youtrack.jetbrains.com/issue/KT-23935 is fixed.
+
+ @Throws(IOException::class)
+ override fun read(sink: Buffer, byteCount: Long): Long = delegate.read(sink, byteCount)
+
+ override fun timeout() = delegate.timeout()
+
+ @Throws(IOException::class)
+ override fun close() = delegate.close()
+
+ override fun toString() = "${javaClass.simpleName}($delegate)"
+
+ @JvmName("-deprecated_delegate")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "delegate"),
+ level = DeprecationLevel.ERROR
+ )
+ fun delegate() = delegate
+}
diff --git a/okio/src/jvmMain/kotlin/okio/ForwardingTimeout.kt b/okio/src/jvmMain/kotlin/okio/ForwardingTimeout.kt
new file mode 100644
index 00000000..23d83aab
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/ForwardingTimeout.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+/** A [Timeout] which forwards calls to another. Useful for subclassing. */
+open class ForwardingTimeout(
+ @get:JvmName("delegate")
+ @set:JvmSynthetic // So .java callers get the setter that returns this.
+ var delegate: Timeout
+) : Timeout() {
+
+ // For backwards compatibility with Okio 1.x, this exists so it can return `ForwardingTimeout`.
+ fun setDelegate(delegate: Timeout): ForwardingTimeout {
+ this.delegate = delegate
+ return this
+ }
+
+ override fun timeout(timeout: Long, unit: TimeUnit) = delegate.timeout(timeout, unit)
+
+ override fun timeoutNanos() = delegate.timeoutNanos()
+
+ override fun hasDeadline() = delegate.hasDeadline()
+
+ override fun deadlineNanoTime() = delegate.deadlineNanoTime()
+
+ override fun deadlineNanoTime(deadlineNanoTime: Long) = delegate.deadlineNanoTime(
+ deadlineNanoTime
+ )
+
+ override fun clearTimeout() = delegate.clearTimeout()
+
+ override fun clearDeadline() = delegate.clearDeadline()
+
+ @Throws(IOException::class)
+ override fun throwIfReached() = delegate.throwIfReached()
+}
diff --git a/okio/src/jvmMain/kotlin/okio/GzipSink.kt b/okio/src/jvmMain/kotlin/okio/GzipSink.kt
new file mode 100644
index 00000000..db87dafa
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/GzipSink.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("-GzipSinkExtensions")
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package okio
+
+import java.io.IOException
+import java.util.zip.CRC32
+import java.util.zip.Deflater
+import java.util.zip.Deflater.DEFAULT_COMPRESSION
+
+/**
+ * A sink that uses [GZIP](http://www.ietf.org/rfc/rfc1952.txt) to
+ * compress written data to another sink.
+ *
+ * ### Sync flush
+ *
+ * Aggressive flushing of this stream may result in reduced compression. Each
+ * call to [flush] immediately compresses all currently-buffered data;
+ * this early compression may be less effective than compression performed
+ * without flushing.
+ *
+ * This is equivalent to using [Deflater] with the sync flush option.
+ * This class does not offer any partial flush mechanism. For best performance,
+ * only call [flush] when application behavior requires it.
+ */
+class GzipSink(sink: Sink) : Sink {
+ /** Sink into which the GZIP format is written. */
+ private val sink = RealBufferedSink(sink)
+
+ /** The deflater used to compress the body. */
+ @get:JvmName("deflater")
+ val deflater = Deflater(DEFAULT_COMPRESSION, true /* No wrap */)
+
+ /**
+ * The deflater sink takes care of moving data between decompressed source and
+ * compressed sink buffers.
+ */
+ private val deflaterSink = DeflaterSink(this.sink, deflater)
+
+ private var closed = false
+
+ /** Checksum calculated for the compressed body. */
+ private val crc = CRC32()
+
+ init {
+ // Write the Gzip header directly into the buffer for the sink to avoid handling IOException.
+ this.sink.buffer.apply {
+ writeShort(0x1f8b) // Two-byte Gzip ID.
+ writeByte(0x08) // 8 == Deflate compression method.
+ writeByte(0x00) // No flags.
+ writeInt(0x00) // No modification time.
+ writeByte(0x00) // No extra flags.
+ writeByte(0x00) // No OS.
+ }
+ }
+
+ @Throws(IOException::class)
+ override fun write(source: Buffer, byteCount: Long) {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ if (byteCount == 0L) return
+
+ updateCrc(source, byteCount)
+ deflaterSink.write(source, byteCount)
+ }
+
+ @Throws(IOException::class)
+ override fun flush() = deflaterSink.flush()
+
+ override fun timeout(): Timeout = sink.timeout()
+
+ @Throws(IOException::class)
+ override fun close() {
+ if (closed) return
+
+ // This method delegates to the DeflaterSink for finishing the deflate process
+ // but keeps responsibility for releasing the deflater's resources. This is
+ // necessary because writeFooter needs to query the processed byte count which
+ // only works when the deflater is still open.
+
+ var thrown: Throwable? = null
+ try {
+ deflaterSink.finishDeflate()
+ writeFooter()
+ } catch (e: Throwable) {
+ thrown = e
+ }
+
+ try {
+ deflater.end()
+ } catch (e: Throwable) {
+ if (thrown == null) thrown = e
+ }
+
+ try {
+ sink.close()
+ } catch (e: Throwable) {
+ if (thrown == null) thrown = e
+ }
+
+ closed = true
+
+ if (thrown != null) throw thrown
+ }
+
+ private fun writeFooter() {
+ sink.writeIntLe(crc.value.toInt()) // CRC of original data.
+ sink.writeIntLe(deflater.bytesRead.toInt()) // Length of original data.
+ }
+
+ /** Updates the CRC with the given bytes. */
+ private fun updateCrc(buffer: Buffer, byteCount: Long) {
+ var head = buffer.head!!
+ var remaining = byteCount
+ while (remaining > 0) {
+ val segmentLength = minOf(remaining, head.limit - head.pos).toInt()
+ crc.update(head.data, head.pos, segmentLength)
+ remaining -= segmentLength
+ head = head.next!!
+ }
+ }
+
+ @JvmName("-deprecated_deflater")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "deflater"),
+ level = DeprecationLevel.ERROR
+ )
+ fun deflater() = deflater
+}
+
+/**
+ * Returns a [GzipSink] that gzip-compresses to this [Sink] while writing.
+ *
+ * @see GzipSource
+ */
+inline fun Sink.gzip() = GzipSink(this)
diff --git a/okio/src/jvmMain/kotlin/okio/GzipSource.kt b/okio/src/jvmMain/kotlin/okio/GzipSource.kt
new file mode 100644
index 00000000..ff1e3d32
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/GzipSource.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("-GzipSourceExtensions")
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package okio
+
+import java.io.EOFException
+import java.io.IOException
+import java.util.zip.CRC32
+import java.util.zip.Inflater
+
+/**
+ * A source that uses [GZIP](http://www.ietf.org/rfc/rfc1952.txt) to
+ * decompress data read from another source.
+ */
+class GzipSource(source: Source) : Source {
+
+ /** The current section. Always progresses forward. */
+ private var section = SECTION_HEADER
+
+ /**
+ * Our source should yield a GZIP header (which we consume directly), followed
+ * by deflated bytes (which we consume via an InflaterSource), followed by a
+ * GZIP trailer (which we also consume directly).
+ */
+ private val source = RealBufferedSource(source)
+
+ /** The inflater used to decompress the deflated body. */
+ private val inflater = Inflater(true)
+
+ /**
+ * The inflater source takes care of moving data between compressed source and
+ * decompressed sink buffers.
+ */
+ private val inflaterSource = InflaterSource(this.source, inflater)
+
+ /** Checksum used to check both the GZIP header and decompressed body. */
+ private val crc = CRC32()
+
+ @Throws(IOException::class)
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ if (byteCount == 0L) return 0L
+
+ // If we haven't consumed the header, we must consume it before anything else.
+ if (section == SECTION_HEADER) {
+ consumeHeader()
+ section = SECTION_BODY
+ }
+
+ // Attempt to read at least a byte of the body. If we do, we're done.
+ if (section == SECTION_BODY) {
+ val offset = sink.size
+ val result = inflaterSource.read(sink, byteCount)
+ if (result != -1L) {
+ updateCrc(sink, offset, result)
+ return result
+ }
+ section = SECTION_TRAILER
+ }
+
+ // The body is exhausted; time to read the trailer. We always consume the
+ // trailer before returning a -1 exhausted result; that way if you read to
+ // the end of a GzipSource you guarantee that the CRC has been checked.
+ if (section == SECTION_TRAILER) {
+ consumeTrailer()
+ section = SECTION_DONE
+
+ // Gzip streams self-terminate: they return -1 before their underlying
+ // source returns -1. Here we attempt to force the underlying stream to
+ // return -1 which may trigger it to release its resources. If it doesn't
+ // return -1, then our Gzip data finished prematurely!
+ if (!source.exhausted()) {
+ throw IOException("gzip finished without exhausting source")
+ }
+ }
+
+ return -1
+ }
+
+ @Throws(IOException::class)
+ private fun consumeHeader() {
+ // Read the 10-byte header. We peek at the flags byte first so we know if we
+ // need to CRC the entire header. Then we read the magic ID1ID2 sequence.
+ // We can skip everything else in the first 10 bytes.
+ // +---+---+---+---+---+---+---+---+---+---+
+ // |ID1|ID2|CM |FLG| MTIME |XFL|OS | (more-->)
+ // +---+---+---+---+---+---+---+---+---+---+
+ source.require(10)
+ val flags = source.buffer[3].toInt()
+ val fhcrc = flags.getBit(FHCRC)
+ if (fhcrc) updateCrc(source.buffer, 0, 10)
+
+ val id1id2 = source.readShort()
+ checkEqual("ID1ID2", 0x1f8b, id1id2.toInt())
+ source.skip(8)
+
+ // Skip optional extra fields.
+ // +---+---+=================================+
+ // | XLEN |...XLEN bytes of "extra field"...| (more-->)
+ // +---+---+=================================+
+ if (flags.getBit(FEXTRA)) {
+ source.require(2)
+ if (fhcrc) updateCrc(source.buffer, 0, 2)
+ val xlen = source.buffer.readShortLe().toLong()
+ source.require(xlen)
+ if (fhcrc) updateCrc(source.buffer, 0, xlen)
+ source.skip(xlen)
+ }
+
+ // Skip an optional 0-terminated name.
+ // +=========================================+
+ // |...original file name, zero-terminated...| (more-->)
+ // +=========================================+
+ if (flags.getBit(FNAME)) {
+ val index = source.indexOf(0)
+ if (index == -1L) throw EOFException()
+ if (fhcrc) updateCrc(source.buffer, 0, index + 1)
+ source.skip(index + 1)
+ }
+
+ // Skip an optional 0-terminated comment.
+ // +===================================+
+ // |...file comment, zero-terminated...| (more-->)
+ // +===================================+
+ if (flags.getBit(FCOMMENT)) {
+ val index = source.indexOf(0)
+ if (index == -1L) throw EOFException()
+ if (fhcrc) updateCrc(source.buffer, 0, index + 1)
+ source.skip(index + 1)
+ }
+
+ // Confirm the optional header CRC.
+ // +---+---+
+ // | CRC16 |
+ // +---+---+
+ if (fhcrc) {
+ checkEqual("FHCRC", source.readShortLe().toInt(), crc.value.toShort().toInt())
+ crc.reset()
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun consumeTrailer() {
+ // Read the eight-byte trailer. Confirm the body's CRC and size.
+ // +---+---+---+---+---+---+---+---+
+ // | CRC32 | ISIZE |
+ // +---+---+---+---+---+---+---+---+
+ checkEqual("CRC", source.readIntLe(), crc.value.toInt())
+ checkEqual("ISIZE", source.readIntLe(), inflater.bytesWritten.toInt())
+ }
+
+ override fun timeout(): Timeout = source.timeout()
+
+ @Throws(IOException::class)
+ override fun close() = inflaterSource.close()
+
+ /** Updates the CRC with the given bytes. */
+ private fun updateCrc(buffer: Buffer, offset: Long, byteCount: Long) {
+ var offset = offset
+ var byteCount = byteCount
+ // Skip segments that we aren't checksumming.
+ var s = buffer.head!!
+ while (offset >= s.limit - s.pos) {
+ offset -= s.limit - s.pos
+ s = s.next!!
+ }
+
+ // Checksum one segment at a time.
+ while (byteCount > 0) {
+ val pos = (s.pos + offset).toInt()
+ val toUpdate = minOf(s.limit - pos, byteCount).toInt()
+ crc.update(s.data, pos, toUpdate)
+ byteCount -= toUpdate
+ offset = 0
+ s = s.next!!
+ }
+ }
+
+ private fun checkEqual(name: String, expected: Int, actual: Int) {
+ if (actual != expected) {
+ throw IOException("%s: actual 0x%08x != expected 0x%08x".format(name, actual, expected))
+ }
+ }
+}
+
+private inline fun Int.getBit(bit: Int) = this shr bit and 1 == 1
+
+private const val FHCRC = 1
+private const val FEXTRA = 2
+private const val FNAME = 3
+private const val FCOMMENT = 4
+
+private const val SECTION_HEADER: Byte = 0
+private const val SECTION_BODY: Byte = 1
+private const val SECTION_TRAILER: Byte = 2
+private const val SECTION_DONE: Byte = 3
+
+/**
+ * Returns a [GzipSource] that gzip-decompresses this [Source] while reading.
+ *
+ * @see GzipSource
+ */
+inline fun Source.gzip() = GzipSource(this)
diff --git a/okio/src/jvmMain/kotlin/okio/HashingSink.kt b/okio/src/jvmMain/kotlin/okio/HashingSink.kt
new file mode 100644
index 00000000..36bfd2b8
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/HashingSink.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import java.security.InvalidKeyException
+import java.security.MessageDigest
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+
+/**
+ * A sink that computes a hash of the full stream of bytes it has accepted. To use, create an
+ * instance with your preferred hash algorithm. Write all of the data to the sink and then call
+ * [hash] to compute the final hash value.
+ *
+ * In this example we use `HashingSink` with a [BufferedSink] to make writing to the
+ * sink easier.
+ * ```
+ * HashingSink hashingSink = HashingSink.sha256(s);
+ * BufferedSink bufferedSink = Okio.buffer(hashingSink);
+ *
+ * ... // Write to bufferedSink and either flush or close it.
+ *
+ * ByteString hash = hashingSink.hash();
+ * ```
+ */
+actual class HashingSink : ForwardingSink, Sink { // Need to explicitly declare sink pending fix for https://youtrack.jetbrains.com/issue/KT-20641
+ private val messageDigest: MessageDigest?
+ private val mac: Mac?
+
+ internal constructor(sink: Sink, digest: MessageDigest) : super(sink) {
+ this.messageDigest = digest
+ this.mac = null
+ }
+
+ internal constructor(sink: Sink, algorithm: String) : this(sink, MessageDigest.getInstance(algorithm))
+
+ internal constructor(sink: Sink, mac: Mac) : super(sink) {
+ this.mac = mac
+ this.messageDigest = null
+ }
+
+ internal constructor(sink: Sink, key: ByteString, algorithm: String) : this(
+ sink,
+ try {
+ Mac.getInstance(algorithm).apply {
+ init(SecretKeySpec(key.toByteArray(), algorithm))
+ }
+ } catch (e: InvalidKeyException) {
+ throw IllegalArgumentException(e)
+ }
+ )
+
+ @Throws(IOException::class)
+ override fun write(source: Buffer, byteCount: Long) {
+ checkOffsetAndCount(source.size, 0, byteCount)
+
+ // Hash byteCount bytes from the prefix of source.
+ var hashedCount = 0L
+ var s = source.head!!
+ while (hashedCount < byteCount) {
+ val toHash = minOf(byteCount - hashedCount, s.limit - s.pos).toInt()
+ if (messageDigest != null) {
+ messageDigest.update(s.data, s.pos, toHash)
+ } else {
+ mac!!.update(s.data, s.pos, toHash)
+ }
+ hashedCount += toHash
+ s = s.next!!
+ }
+
+ // Write those bytes to the sink.
+ super.write(source, byteCount)
+ }
+
+ /**
+ * Returns the hash of the bytes accepted thus far and resets the internal state of this sink.
+ *
+ * **Warning:** This method is not idempotent. Each time this method is called its
+ * internal state is cleared. This starts a new hash with zero bytes accepted.
+ */
+ @get:JvmName("hash")
+ actual val hash: ByteString
+ get() {
+ val result = if (messageDigest != null) messageDigest.digest() else mac!!.doFinal()
+ return ByteString(result)
+ }
+
+ @JvmName("-deprecated_hash")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "hash"),
+ level = DeprecationLevel.ERROR
+ )
+ fun hash() = hash
+
+ actual companion object {
+ /** Returns a sink that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */
+ @JvmStatic
+ actual fun md5(sink: Sink) = HashingSink(sink, "MD5")
+
+ /** Returns a sink that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */
+ @JvmStatic
+ actual fun sha1(sink: Sink) = HashingSink(sink, "SHA-1")
+
+ /** Returns a sink that uses the SHA-256 hash algorithm to produce 256-bit hashes. */
+ @JvmStatic
+ actual fun sha256(sink: Sink) = HashingSink(sink, "SHA-256")
+
+ /** Returns a sink that uses the SHA-512 hash algorithm to produce 512-bit hashes. */
+ @JvmStatic
+ actual fun sha512(sink: Sink) = HashingSink(sink, "SHA-512")
+
+ /** Returns a sink that uses the obsolete SHA-1 HMAC algorithm to produce 160-bit hashes. */
+ @JvmStatic
+ actual fun hmacSha1(sink: Sink, key: ByteString) = HashingSink(sink, key, "HmacSHA1")
+
+ /** Returns a sink that uses the SHA-256 HMAC algorithm to produce 256-bit hashes. */
+ @JvmStatic
+ actual fun hmacSha256(sink: Sink, key: ByteString) = HashingSink(sink, key, "HmacSHA256")
+
+ /** Returns a sink that uses the SHA-512 HMAC algorithm to produce 512-bit hashes. */
+ @JvmStatic
+ actual fun hmacSha512(sink: Sink, key: ByteString) = HashingSink(sink, key, "HmacSHA512")
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/HashingSource.kt b/okio/src/jvmMain/kotlin/okio/HashingSource.kt
new file mode 100644
index 00000000..25b695d7
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/HashingSource.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import java.security.InvalidKeyException
+import java.security.MessageDigest
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+
+/**
+ * A source that computes a hash of the full stream of bytes it has supplied. To use, create an
+ * instance with your preferred hash algorithm. Exhaust the source by reading all of its bytes and
+ * then call [hash] to compute the final hash value.
+ *
+ *
+ * In this example we use `HashingSource` with a [BufferedSource] to make reading
+ * from the source easier.
+ * ```
+ * HashingSource hashingSource = HashingSource.sha256(rawSource);
+ * BufferedSource bufferedSource = Okio.buffer(hashingSource);
+ *
+ * ... // Read all of bufferedSource.
+ *
+ * ByteString hash = hashingSource.hash();
+ * ```
+ */
+actual class HashingSource : ForwardingSource, Source { // Need to explicitly declare source pending fix for https://youtrack.jetbrains.com/issue/KT-20641
+ private val messageDigest: MessageDigest?
+ private val mac: Mac?
+
+ internal constructor(source: Source, digest: MessageDigest) : super(source) {
+ this.messageDigest = digest
+ this.mac = null
+ }
+
+ internal constructor(source: Source, algorithm: String) : this(source, MessageDigest.getInstance(algorithm))
+
+ internal constructor(source: Source, mac: Mac) : super(source) {
+ this.mac = mac
+ this.messageDigest = null
+ }
+
+ internal constructor(source: Source, key: ByteString, algorithm: String) : this(
+ source,
+ try {
+ Mac.getInstance(algorithm).apply {
+ init(SecretKeySpec(key.toByteArray(), algorithm))
+ }
+ } catch (e: InvalidKeyException) {
+ throw IllegalArgumentException(e)
+ }
+ )
+
+ @Throws(IOException::class)
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ val result = super.read(sink, byteCount)
+
+ if (result != -1L) {
+ var start = sink.size - result
+
+ // Find the first segment that has new bytes.
+ var offset = sink.size
+ var s = sink.head!!
+ while (offset > start) {
+ s = s.prev!!
+ offset -= (s.limit - s.pos).toLong()
+ }
+
+ // Hash that segment and all the rest until the end.
+ while (offset < sink.size) {
+ val pos = (s.pos + start - offset).toInt()
+ if (messageDigest != null) {
+ messageDigest.update(s.data, pos, s.limit - pos)
+ } else {
+ mac!!.update(s.data, pos, s.limit - pos)
+ }
+ offset += s.limit - s.pos
+ start = offset
+ s = s.next!!
+ }
+ }
+
+ return result
+ }
+
+ /**
+ * Returns the hash of the bytes supplied thus far and resets the internal state of this source.
+ *
+ * **Warning:** This method is not idempotent. Each time this method is called its
+ * internal state is cleared. This starts a new hash with zero bytes supplied.
+ */
+ @get:JvmName("hash")
+ actual val hash: ByteString
+ get() {
+ val result = if (messageDigest != null) messageDigest.digest() else mac!!.doFinal()
+ return ByteString(result)
+ }
+
+ @JvmName("-deprecated_hash")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "hash"),
+ level = DeprecationLevel.ERROR
+ )
+ fun hash() = hash
+
+ actual companion object {
+ /** Returns a source that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */
+ @JvmStatic
+ actual fun md5(source: Source) = HashingSource(source, "MD5")
+
+ /** Returns a source that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */
+ @JvmStatic
+ actual fun sha1(source: Source) = HashingSource(source, "SHA-1")
+
+ /** Returns a source that uses the SHA-256 hash algorithm to produce 256-bit hashes. */
+ @JvmStatic
+ actual fun sha256(source: Source) = HashingSource(source, "SHA-256")
+
+ /** Returns a source that uses the SHA-512 hash algorithm to produce 512-bit hashes. */
+ @JvmStatic
+ actual fun sha512(source: Source) = HashingSource(source, "SHA-512")
+
+ /** Returns a source that uses the obsolete SHA-1 HMAC algorithm to produce 160-bit hashes. */
+ @JvmStatic
+ actual fun hmacSha1(source: Source, key: ByteString) = HashingSource(source, key, "HmacSHA1")
+
+ /** Returns a source that uses the SHA-256 HMAC algorithm to produce 256-bit hashes. */
+ @JvmStatic
+ actual fun hmacSha256(source: Source, key: ByteString) = HashingSource(source, key, "HmacSHA256")
+
+ /** Returns a source that uses the SHA-512 HMAC algorithm to produce 512-bit hashes. */
+ @JvmStatic
+ actual fun hmacSha512(source: Source, key: ByteString) = HashingSource(source, key, "HmacSHA512")
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/InflaterSource.kt b/okio/src/jvmMain/kotlin/okio/InflaterSource.kt
new file mode 100644
index 00000000..6fe1feb6
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/InflaterSource.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("-InflaterSourceExtensions")
+@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.
+
+package okio
+
+import java.io.IOException
+import java.util.zip.DataFormatException
+import java.util.zip.Inflater
+
+/**
+ * A source that uses [DEFLATE](http://tools.ietf.org/html/rfc1951) to decompress data read from
+ * another source.
+ */
+class InflaterSource
+/**
+ * This internal constructor shares a buffer with its trusted caller. In general we can't share a
+ * `BufferedSource` because the inflater holds input bytes until they are inflated.
+ */
+internal constructor(private val source: BufferedSource, private val inflater: Inflater) : Source {
+
+ /**
+ * When we call Inflater.setInput(), the inflater keeps our byte array until it needs input again.
+ * This tracks how many bytes the inflater is currently holding on to.
+ */
+ private var bufferBytesHeldByInflater = 0
+ private var closed = false
+
+ constructor(source: Source, inflater: Inflater) : this(source.buffer(), inflater)
+
+ @Throws(IOException::class)
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ while (true) {
+ val bytesInflated = readOrInflate(sink, byteCount)
+ if (bytesInflated > 0) return bytesInflated
+ if (inflater.finished() || inflater.needsDictionary()) return -1L
+ if (source.exhausted()) throw EOFException("source exhausted prematurely")
+ }
+ }
+
+ /**
+ * Consume deflated bytes from the underlying source, and write any inflated bytes to [sink].
+ * Returns the number of inflated bytes written to [sink]. This may return 0L, though it will
+ * always consume 1 or more bytes from the underlying source if it is not exhausted.
+ *
+ * Use this instead of [read] when it is useful to consume the deflated stream even when doing so
+ * doesn't yield inflated bytes.
+ */
+ @Throws(IOException::class)
+ fun readOrInflate(sink: Buffer, byteCount: Long): Long {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ check(!closed) { "closed" }
+ if (byteCount == 0L) return 0L
+
+ try {
+ // Prepare the destination that we'll write into.
+ val tail = sink.writableSegment(1)
+ val toRead = minOf(byteCount, Segment.SIZE - tail.limit).toInt()
+
+ // Prepare the source that we'll read from.
+ refill()
+
+ // Decompress the inflater's compressed data into the sink.
+ val bytesInflated = inflater.inflate(tail.data, tail.limit, toRead)
+
+ // Release consumed bytes from the source.
+ releaseBytesAfterInflate()
+
+ // Track produced bytes in the destination.
+ if (bytesInflated > 0) {
+ tail.limit += bytesInflated
+ sink.size += bytesInflated
+ return bytesInflated.toLong()
+ }
+
+ // We allocated a tail segment but didn't end up needing it. Recycle!
+ if (tail.pos == tail.limit) {
+ sink.head = tail.pop()
+ SegmentPool.recycle(tail)
+ }
+
+ return 0L
+ } catch (e: DataFormatException) {
+ throw IOException(e)
+ }
+ }
+
+ /**
+ * Refills the inflater with compressed data if it needs input. (And only if it needs input).
+ * Returns true if the inflater required input but the source was exhausted.
+ */
+ @Throws(IOException::class)
+ fun refill(): Boolean {
+ if (!inflater.needsInput()) return false
+
+ // If there are no further bytes in the source, we cannot refill.
+ if (source.exhausted()) return true
+
+ // Assign buffer bytes to the inflater.
+ val head = source.buffer.head!!
+ bufferBytesHeldByInflater = head.limit - head.pos
+ inflater.setInput(head.data, head.pos, bufferBytesHeldByInflater)
+ return false
+ }
+
+ /** When the inflater has processed compressed data, remove it from the buffer. */
+ private fun releaseBytesAfterInflate() {
+ if (bufferBytesHeldByInflater == 0) return
+ val toRelease = bufferBytesHeldByInflater - inflater.remaining
+ bufferBytesHeldByInflater -= toRelease
+ source.skip(toRelease.toLong())
+ }
+
+ override fun timeout(): Timeout = source.timeout()
+
+ @Throws(IOException::class)
+ override fun close() {
+ if (closed) return
+ inflater.end()
+ closed = true
+ source.close()
+ }
+}
+
+/**
+ * Returns an [InflaterSource] that DEFLATE-decompresses this [Source] while reading.
+ *
+ * @see InflaterSource
+ */
+inline fun Source.inflate(inflater: Inflater = Inflater()): InflaterSource =
+ InflaterSource(this, inflater)
diff --git a/okio/src/jvmMain/kotlin/okio/JvmOkio.kt b/okio/src/jvmMain/kotlin/okio/JvmOkio.kt
new file mode 100644
index 00000000..25d55166
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/JvmOkio.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Essential APIs for working with Okio. */
+@file:JvmMultifileClass
+@file:JvmName("Okio")
+
+package okio
+
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.net.Socket
+import java.net.SocketTimeoutException
+import java.nio.file.Files
+import java.nio.file.OpenOption
+import java.nio.file.Path
+import java.security.MessageDigest
+import java.util.logging.Level
+import java.util.logging.Logger
+import javax.crypto.Cipher
+import javax.crypto.Mac
+
+/** Returns a sink that writes to `out`. */
+fun OutputStream.sink(): Sink = OutputStreamSink(this, Timeout())
+
+private class OutputStreamSink(
+ private val out: OutputStream,
+ private val timeout: Timeout
+) : Sink {
+
+ override fun write(source: Buffer, byteCount: Long) {
+ checkOffsetAndCount(source.size, 0, byteCount)
+ var remaining = byteCount
+ while (remaining > 0) {
+ timeout.throwIfReached()
+ val head = source.head!!
+ val toCopy = minOf(remaining, head.limit - head.pos).toInt()
+ out.write(head.data, head.pos, toCopy)
+
+ head.pos += toCopy
+ remaining -= toCopy
+ source.size -= toCopy
+
+ if (head.pos == head.limit) {
+ source.head = head.pop()
+ SegmentPool.recycle(head)
+ }
+ }
+ }
+
+ override fun flush() = out.flush()
+
+ override fun close() = out.close()
+
+ override fun timeout() = timeout
+
+ override fun toString() = "sink($out)"
+}
+
+/** Returns a source that reads from `in`. */
+fun InputStream.source(): Source = InputStreamSource(this, Timeout())
+
+private class InputStreamSource(
+ private val input: InputStream,
+ private val timeout: Timeout
+) : Source {
+
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ if (byteCount == 0L) return 0L
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+ try {
+ timeout.throwIfReached()
+ val tail = sink.writableSegment(1)
+ val maxToCopy = minOf(byteCount, Segment.SIZE - tail.limit).toInt()
+ val bytesRead = input.read(tail.data, tail.limit, maxToCopy)
+ if (bytesRead == -1) {
+ if (tail.pos == tail.limit) {
+ // We allocated a tail segment, but didn't end up needing it. Recycle!
+ sink.head = tail.pop()
+ SegmentPool.recycle(tail)
+ }
+ return -1
+ }
+ tail.limit += bytesRead
+ sink.size += bytesRead
+ return bytesRead.toLong()
+ } catch (e: AssertionError) {
+ if (e.isAndroidGetsocknameError) throw IOException(e)
+ throw e
+ }
+ }
+
+ override fun close() = input.close()
+
+ override fun timeout() = timeout
+
+ override fun toString() = "source($input)"
+}
+
+/**
+ * Returns a sink that writes to `socket`. Prefer this over [sink]
+ * because this method honors timeouts. When the socket
+ * write times out, the socket is asynchronously closed by a watchdog thread.
+ */
+@Throws(IOException::class)
+fun Socket.sink(): Sink {
+ val timeout = SocketAsyncTimeout(this)
+ val sink = OutputStreamSink(getOutputStream(), timeout)
+ return timeout.sink(sink)
+}
+
+/**
+ * Returns a source that reads from `socket`. Prefer this over [source]
+ * because this method honors timeouts. When the socket
+ * read times out, the socket is asynchronously closed by a watchdog thread.
+ */
+@Throws(IOException::class)
+fun Socket.source(): Source {
+ val timeout = SocketAsyncTimeout(this)
+ val source = InputStreamSource(getInputStream(), timeout)
+ return timeout.source(source)
+}
+
+private val logger = Logger.getLogger("okio.Okio")
+
+private class SocketAsyncTimeout(private val socket: Socket) : AsyncTimeout() {
+ override fun newTimeoutException(cause: IOException?): IOException {
+ val ioe = SocketTimeoutException("timeout")
+ if (cause != null) {
+ ioe.initCause(cause)
+ }
+ return ioe
+ }
+
+ override fun timedOut() {
+ try {
+ socket.close()
+ } catch (e: Exception) {
+ logger.log(Level.WARNING, "Failed to close timed out socket $socket", e)
+ } catch (e: AssertionError) {
+ if (e.isAndroidGetsocknameError) {
+ // Catch this exception due to a Firmware issue up to android 4.2.2
+ // https://code.google.com/p/android/issues/detail?id=54072
+ logger.log(Level.WARNING, "Failed to close timed out socket $socket", e)
+ } else {
+ throw e
+ }
+ }
+ }
+}
+
+/** Returns a sink that writes to `file`. */
+@JvmOverloads
+@Throws(FileNotFoundException::class)
+fun File.sink(append: Boolean = false): Sink = FileOutputStream(this, append).sink()
+
+/** Returns a sink that writes to `file`. */
+@Throws(FileNotFoundException::class)
+fun File.appendingSink(): Sink = FileOutputStream(this, true).sink()
+
+/** Returns a source that reads from `file`. */
+@Throws(FileNotFoundException::class)
+fun File.source(): Source = inputStream().source()
+
+/** Returns a source that reads from `path`. */
+@Throws(IOException::class)
+@IgnoreJRERequirement // Can only be invoked on Java 7+.
+fun Path.sink(vararg options: OpenOption): Sink =
+ Files.newOutputStream(this, *options).sink()
+
+/** Returns a sink that writes to `path`. */
+@Throws(IOException::class)
+@IgnoreJRERequirement // Can only be invoked on Java 7+.
+fun Path.source(vararg options: OpenOption): Source =
+ Files.newInputStream(this, *options).source()
+
+/**
+ * Returns a sink that uses [cipher] to encrypt or decrypt [this].
+ *
+ * @throws IllegalArgumentException if [cipher] isn't a block cipher.
+ */
+fun Sink.cipherSink(cipher: Cipher): CipherSink = CipherSink(this.buffer(), cipher)
+
+/**
+ * Returns a source that uses [cipher] to encrypt or decrypt [this].
+ *
+ * @throws IllegalArgumentException if [cipher] isn't a block cipher.
+ */
+fun Source.cipherSource(cipher: Cipher): CipherSource = CipherSource(this.buffer(), cipher)
+
+/**
+ * Returns a sink that uses [mac] to hash [this].
+ */
+fun Sink.hashingSink(mac: Mac): HashingSink = HashingSink(this, mac)
+
+/**
+ * Returns a source that uses [mac] to hash [this].
+ */
+fun Source.hashingSource(mac: Mac): HashingSource = HashingSource(this, mac)
+
+/**
+ * Returns a sink that uses [digest] to hash [this].
+ */
+fun Sink.hashingSink(digest: MessageDigest): HashingSink = HashingSink(this, digest)
+
+/**
+ * Returns a source that uses [digest] to hash [this].
+ */
+fun Source.hashingSource(digest: MessageDigest): HashingSource = HashingSource(this, digest)
+
+/**
+ * Returns true if this error is due to a firmware bug fixed after Android 4.2.2.
+ * https://code.google.com/p/android/issues/detail?id=54072
+ */
+internal val AssertionError.isAndroidGetsocknameError: Boolean get() {
+ return cause != null && message?.contains("getsockname failed") ?: false
+}
diff --git a/okio/src/jvmMain/kotlin/okio/Pipe.kt b/okio/src/jvmMain/kotlin/okio/Pipe.kt
new file mode 100644
index 00000000..43c23bfd
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/Pipe.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * A source and a sink that are attached. The sink's output is the source's input. Typically each
+ * is accessed by its own thread: a producer thread writes data to the sink and a consumer thread
+ * reads data from the source.
+ *
+ * This class uses a buffer to decouple source and sink. This buffer has a user-specified maximum
+ * size. When a producer thread outruns its consumer the buffer fills up and eventually writes to
+ * the sink will block until the consumer has caught up. Symmetrically, if a consumer outruns its
+ * producer reads block until there is data to be read. Limits on the amount of time spent waiting
+ * for the other party can be configured with [timeouts][Timeout] on the source and the
+ * sink.
+ *
+ * When the sink is closed, source reads will continue to complete normally until the buffer has
+ * been exhausted. At that point reads will return -1, indicating the end of the stream. But if the
+ * source is closed first, writes to the sink will immediately fail with an [IOException].
+ *
+ * A pipe may be canceled to immediately fail writes to the sink and reads from the source.
+ */
+class Pipe(internal val maxBufferSize: Long) {
+ internal val buffer = Buffer()
+ internal var canceled = false
+ internal var sinkClosed = false
+ internal var sourceClosed = false
+ internal var foldedSink: Sink? = null
+
+ init {
+ require(maxBufferSize >= 1L) { "maxBufferSize < 1: $maxBufferSize" }
+ }
+
+ @get:JvmName("sink")
+ val sink = object : Sink {
+ private val timeout = Timeout()
+
+ override fun write(source: Buffer, byteCount: Long) {
+ var byteCount = byteCount
+ var delegate: Sink? = null
+ synchronized(buffer) {
+ check(!sinkClosed) { "closed" }
+ if (canceled) throw IOException("canceled")
+
+ while (byteCount > 0) {
+ foldedSink?.let {
+ delegate = it
+ return@synchronized
+ }
+
+ if (sourceClosed) throw IOException("source is closed")
+
+ val bufferSpaceAvailable = maxBufferSize - buffer.size
+ if (bufferSpaceAvailable == 0L) {
+ timeout.waitUntilNotified(buffer) // Wait until the source drains the buffer.
+ if (canceled) throw IOException("canceled")
+ continue
+ }
+
+ val bytesToWrite = minOf(bufferSpaceAvailable, byteCount)
+ buffer.write(source, bytesToWrite)
+ byteCount -= bytesToWrite
+ (buffer as Object).notifyAll() // Notify the source that it can resume reading.
+ }
+ }
+
+ delegate?.forward { write(source, byteCount) }
+ }
+
+ override fun flush() {
+ var delegate: Sink? = null
+ synchronized(buffer) {
+ check(!sinkClosed) { "closed" }
+ if (canceled) throw IOException("canceled")
+
+ foldedSink?.let {
+ delegate = it
+ return@synchronized
+ }
+
+ if (sourceClosed && buffer.size > 0L) {
+ throw IOException("source is closed")
+ }
+ }
+
+ delegate?.forward { flush() }
+ }
+
+ override fun close() {
+ var delegate: Sink? = null
+ synchronized(buffer) {
+ if (sinkClosed) return
+
+ foldedSink?.let {
+ delegate = it
+ return@synchronized
+ }
+
+ if (sourceClosed && buffer.size > 0L) throw IOException("source is closed")
+ sinkClosed = true
+ (buffer as Object).notifyAll() // Notify the source that no more bytes are coming.
+ }
+
+ delegate?.forward { close() }
+ }
+
+ override fun timeout(): Timeout = timeout
+ }
+
+ @get:JvmName("source")
+ val source = object : Source {
+ private val timeout = Timeout()
+
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ synchronized(buffer) {
+ check(!sourceClosed) { "closed" }
+ if (canceled) throw IOException("canceled")
+
+ while (buffer.size == 0L) {
+ if (sinkClosed) return -1L
+ timeout.waitUntilNotified(buffer) // Wait until the sink fills the buffer.
+ if (canceled) throw IOException("canceled")
+ }
+
+ val result = buffer.read(sink, byteCount)
+ (buffer as Object).notifyAll() // Notify the sink that it can resume writing.
+ return result
+ }
+ }
+
+ override fun close() {
+ synchronized(buffer) {
+ sourceClosed = true
+ (buffer as Object).notifyAll() // Notify the sink that no more bytes are desired.
+ }
+ }
+
+ override fun timeout(): Timeout = timeout
+ }
+
+ /**
+ * Writes any buffered contents of this pipe to `sink`, then replace this pipe's source with
+ * `sink`. This pipe's source is closed and attempts to read it will throw an
+ * [IllegalStateException].
+ *
+ * This method must not be called while concurrently accessing this pipe's source. It is safe,
+ * however, to call this while concurrently writing this pipe's sink.
+ */
+ @Throws(IOException::class)
+ fun fold(sink: Sink) {
+ while (true) {
+ // Either the buffer is empty and we can swap and return. Or the buffer is non-empty and we
+ // must copy it to sink without holding any locks, then try it all again.
+ var closed = false
+ lateinit var sinkBuffer: Buffer
+ synchronized(buffer) {
+ check(foldedSink == null) { "sink already folded" }
+
+ if (canceled) {
+ foldedSink = sink
+ throw IOException("canceled")
+ }
+
+ if (buffer.exhausted()) {
+ sourceClosed = true
+ foldedSink = sink
+ return@fold
+ }
+
+ closed = sinkClosed
+ sinkBuffer = Buffer()
+ sinkBuffer.write(buffer, buffer.size)
+ (buffer as Object).notifyAll() // Notify the sink that it can resume writing.
+ }
+
+ var success = false
+ try {
+ sink.write(sinkBuffer, sinkBuffer.size)
+ if (closed) {
+ sink.close()
+ } else {
+ sink.flush()
+ }
+ success = true
+ } finally {
+ if (!success) {
+ synchronized(buffer) {
+ sourceClosed = true
+ (buffer as Object).notifyAll() // Notify the sink that it can resume writing.
+ }
+ }
+ }
+ }
+ }
+
+ private inline fun Sink.forward(block: Sink.() -> Unit) {
+ this.timeout().intersectWith(this@Pipe.sink.timeout()) { this.block() }
+ }
+
+ @JvmName("-deprecated_sink")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "sink"),
+ level = DeprecationLevel.ERROR
+ )
+ fun sink() = sink
+
+ @JvmName("-deprecated_source")
+ @Deprecated(
+ message = "moved to val",
+ replaceWith = ReplaceWith(expression = "source"),
+ level = DeprecationLevel.ERROR
+ )
+ fun source() = source
+
+ /**
+ * Fail any in-flight and future operations. After canceling:
+ *
+ * * Any attempt to write or flush [sink] will fail immediately with an [IOException].
+ * * Any attempt to read [source] will fail immediately with an [IOException].
+ * * Any attempt to [fold] will fail immediately with an [IOException].
+ *
+ * Closing the source and the sink will complete normally even after a pipe has been canceled. If
+ * this sink has been folded, closing it will close the folded sink. This operation may block.
+ *
+ * This operation may be called by any thread at any time. It is safe to call concurrently while
+ * operating on the source or the sink.
+ */
+ fun cancel() {
+ synchronized(buffer) {
+ canceled = true
+ buffer.clear()
+ (buffer as Object).notifyAll() // Notify the source and sink that they're canceled.
+ }
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/RealBufferedSink.kt b/okio/src/jvmMain/kotlin/okio/RealBufferedSink.kt
new file mode 100644
index 00000000..7df3f937
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/RealBufferedSink.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.commonClose
+import okio.internal.commonEmit
+import okio.internal.commonEmitCompleteSegments
+import okio.internal.commonFlush
+import okio.internal.commonTimeout
+import okio.internal.commonToString
+import okio.internal.commonWrite
+import okio.internal.commonWriteAll
+import okio.internal.commonWriteByte
+import okio.internal.commonWriteDecimalLong
+import okio.internal.commonWriteHexadecimalUnsignedLong
+import okio.internal.commonWriteInt
+import okio.internal.commonWriteIntLe
+import okio.internal.commonWriteLong
+import okio.internal.commonWriteLongLe
+import okio.internal.commonWriteShort
+import okio.internal.commonWriteShortLe
+import okio.internal.commonWriteUtf8
+import okio.internal.commonWriteUtf8CodePoint
+import java.io.IOException
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import java.nio.charset.Charset
+
+internal actual class RealBufferedSink actual constructor(
+ @JvmField actual val sink: Sink
+) : BufferedSink {
+ @JvmField val bufferField = Buffer()
+ @JvmField actual var closed: Boolean = false
+
+ @Suppress("OVERRIDE_BY_INLINE") // Prevent internal code from calling the getter.
+ override val buffer: Buffer
+ inline get() = bufferField
+
+ override fun buffer() = bufferField
+
+ override fun write(source: Buffer, byteCount: Long) = commonWrite(source, byteCount)
+ override fun write(byteString: ByteString) = commonWrite(byteString)
+ override fun write(byteString: ByteString, offset: Int, byteCount: Int) =
+ commonWrite(byteString, offset, byteCount)
+ override fun writeUtf8(string: String) = commonWriteUtf8(string)
+ override fun writeUtf8(string: String, beginIndex: Int, endIndex: Int) =
+ commonWriteUtf8(string, beginIndex, endIndex)
+
+ override fun writeUtf8CodePoint(codePoint: Int) = commonWriteUtf8CodePoint(codePoint)
+
+ override fun writeString(string: String, charset: Charset): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeString(string, charset)
+ return emitCompleteSegments()
+ }
+
+ override fun writeString(
+ string: String,
+ beginIndex: Int,
+ endIndex: Int,
+ charset: Charset
+ ): BufferedSink {
+ check(!closed) { "closed" }
+ buffer.writeString(string, beginIndex, endIndex, charset)
+ return emitCompleteSegments()
+ }
+
+ override fun write(source: ByteArray) = commonWrite(source)
+ override fun write(source: ByteArray, offset: Int, byteCount: Int) =
+ commonWrite(source, offset, byteCount)
+
+ override fun write(source: ByteBuffer): Int {
+ check(!closed) { "closed" }
+ val result = buffer.write(source)
+ emitCompleteSegments()
+ return result
+ }
+
+ override fun writeAll(source: Source) = commonWriteAll(source)
+ override fun write(source: Source, byteCount: Long): BufferedSink = commonWrite(source, byteCount)
+ override fun writeByte(b: Int) = commonWriteByte(b)
+ override fun writeShort(s: Int) = commonWriteShort(s)
+ override fun writeShortLe(s: Int) = commonWriteShortLe(s)
+ override fun writeInt(i: Int) = commonWriteInt(i)
+ override fun writeIntLe(i: Int) = commonWriteIntLe(i)
+ override fun writeLong(v: Long) = commonWriteLong(v)
+ override fun writeLongLe(v: Long) = commonWriteLongLe(v)
+ override fun writeDecimalLong(v: Long) = commonWriteDecimalLong(v)
+ override fun writeHexadecimalUnsignedLong(v: Long) = commonWriteHexadecimalUnsignedLong(v)
+ override fun emitCompleteSegments() = commonEmitCompleteSegments()
+ override fun emit() = commonEmit()
+
+ override fun outputStream(): OutputStream {
+ return object : OutputStream() {
+ override fun write(b: Int) {
+ if (closed) throw IOException("closed")
+ buffer.writeByte(b.toByte().toInt())
+ emitCompleteSegments()
+ }
+
+ override fun write(data: ByteArray, offset: Int, byteCount: Int) {
+ if (closed) throw IOException("closed")
+ buffer.write(data, offset, byteCount)
+ emitCompleteSegments()
+ }
+
+ override fun flush() {
+ // For backwards compatibility, a flush() on a closed stream is a no-op.
+ if (!closed) {
+ this@RealBufferedSink.flush()
+ }
+ }
+
+ override fun close() = this@RealBufferedSink.close()
+
+ override fun toString() = "${this@RealBufferedSink}.outputStream()"
+ }
+ }
+
+ override fun flush() = commonFlush()
+
+ override fun isOpen() = !closed
+
+ override fun close() = commonClose()
+ override fun timeout() = commonTimeout()
+ override fun toString() = commonToString()
+}
diff --git a/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt b/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt
new file mode 100644
index 00000000..109ef140
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/RealBufferedSource.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.commonClose
+import okio.internal.commonExhausted
+import okio.internal.commonIndexOf
+import okio.internal.commonIndexOfElement
+import okio.internal.commonPeek
+import okio.internal.commonRangeEquals
+import okio.internal.commonRead
+import okio.internal.commonReadAll
+import okio.internal.commonReadByte
+import okio.internal.commonReadByteArray
+import okio.internal.commonReadByteString
+import okio.internal.commonReadDecimalLong
+import okio.internal.commonReadFully
+import okio.internal.commonReadHexadecimalUnsignedLong
+import okio.internal.commonReadInt
+import okio.internal.commonReadIntLe
+import okio.internal.commonReadLong
+import okio.internal.commonReadLongLe
+import okio.internal.commonReadShort
+import okio.internal.commonReadShortLe
+import okio.internal.commonReadUtf8
+import okio.internal.commonReadUtf8CodePoint
+import okio.internal.commonReadUtf8Line
+import okio.internal.commonReadUtf8LineStrict
+import okio.internal.commonRequest
+import okio.internal.commonRequire
+import okio.internal.commonSelect
+import okio.internal.commonSkip
+import okio.internal.commonTimeout
+import okio.internal.commonToString
+import java.io.IOException
+import java.io.InputStream
+import java.nio.ByteBuffer
+import java.nio.charset.Charset
+
+internal actual class RealBufferedSource actual constructor(
+ @JvmField actual val source: Source
+) : BufferedSource {
+ @JvmField val bufferField = Buffer()
+ @JvmField actual var closed: Boolean = false
+
+ @Suppress("OVERRIDE_BY_INLINE") // Prevent internal code from calling the getter.
+ override val buffer: Buffer
+ inline get() = bufferField
+
+ override fun buffer() = bufferField
+
+ override fun read(sink: Buffer, byteCount: Long): Long = commonRead(sink, byteCount)
+ override fun exhausted(): Boolean = commonExhausted()
+ override fun require(byteCount: Long): Unit = commonRequire(byteCount)
+ override fun request(byteCount: Long): Boolean = commonRequest(byteCount)
+ override fun readByte(): Byte = commonReadByte()
+ override fun readByteString(): ByteString = commonReadByteString()
+ override fun readByteString(byteCount: Long): ByteString = commonReadByteString(byteCount)
+ override fun select(options: Options): Int = commonSelect(options)
+ override fun readByteArray(): ByteArray = commonReadByteArray()
+ override fun readByteArray(byteCount: Long): ByteArray = commonReadByteArray(byteCount)
+ override fun read(sink: ByteArray): Int = read(sink, 0, sink.size)
+ override fun readFully(sink: ByteArray): Unit = commonReadFully(sink)
+ override fun read(sink: ByteArray, offset: Int, byteCount: Int): Int =
+ commonRead(sink, offset, byteCount)
+
+ override fun read(sink: ByteBuffer): Int {
+ if (buffer.size == 0L) {
+ val read = source.read(buffer, Segment.SIZE.toLong())
+ if (read == -1L) return -1
+ }
+
+ return buffer.read(sink)
+ }
+
+ override fun readFully(sink: Buffer, byteCount: Long): Unit = commonReadFully(sink, byteCount)
+ override fun readAll(sink: Sink): Long = commonReadAll(sink)
+ override fun readUtf8(): String = commonReadUtf8()
+ override fun readUtf8(byteCount: Long): String = commonReadUtf8(byteCount)
+
+ override fun readString(charset: Charset): String {
+ buffer.writeAll(source)
+ return buffer.readString(charset)
+ }
+
+ override fun readString(byteCount: Long, charset: Charset): String {
+ require(byteCount)
+ return buffer.readString(byteCount, charset)
+ }
+
+ override fun readUtf8Line(): String? = commonReadUtf8Line()
+ override fun readUtf8LineStrict() = readUtf8LineStrict(Long.MAX_VALUE)
+ override fun readUtf8LineStrict(limit: Long): String = commonReadUtf8LineStrict(limit)
+ override fun readUtf8CodePoint(): Int = commonReadUtf8CodePoint()
+ override fun readShort(): Short = commonReadShort()
+ override fun readShortLe(): Short = commonReadShortLe()
+ override fun readInt(): Int = commonReadInt()
+ override fun readIntLe(): Int = commonReadIntLe()
+ override fun readLong(): Long = commonReadLong()
+ override fun readLongLe(): Long = commonReadLongLe()
+ override fun readDecimalLong(): Long = commonReadDecimalLong()
+ override fun readHexadecimalUnsignedLong(): Long = commonReadHexadecimalUnsignedLong()
+ override fun skip(byteCount: Long): Unit = commonSkip(byteCount)
+ override fun indexOf(b: Byte): Long = indexOf(b, 0L, Long.MAX_VALUE)
+ override fun indexOf(b: Byte, fromIndex: Long): Long = indexOf(b, fromIndex, Long.MAX_VALUE)
+ override fun indexOf(b: Byte, fromIndex: Long, toIndex: Long): Long =
+ commonIndexOf(b, fromIndex, toIndex)
+
+ override fun indexOf(bytes: ByteString): Long = indexOf(bytes, 0L)
+ override fun indexOf(bytes: ByteString, fromIndex: Long): Long = commonIndexOf(bytes, fromIndex)
+ override fun indexOfElement(targetBytes: ByteString): Long = indexOfElement(targetBytes, 0L)
+ override fun indexOfElement(targetBytes: ByteString, fromIndex: Long): Long =
+ commonIndexOfElement(targetBytes, fromIndex)
+
+ override fun rangeEquals(offset: Long, bytes: ByteString) = rangeEquals(
+ offset, bytes, 0,
+ bytes.size
+ )
+
+ override fun rangeEquals(
+ offset: Long,
+ bytes: ByteString,
+ bytesOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, bytes, bytesOffset, byteCount)
+
+ override fun peek(): BufferedSource = commonPeek()
+
+ override fun inputStream(): InputStream {
+ return object : InputStream() {
+ override fun read(): Int {
+ if (closed) throw IOException("closed")
+ if (buffer.size == 0L) {
+ val count = source.read(buffer, Segment.SIZE.toLong())
+ if (count == -1L) return -1
+ }
+ return buffer.readByte() and 0xff
+ }
+
+ override fun read(data: ByteArray, offset: Int, byteCount: Int): Int {
+ if (closed) throw IOException("closed")
+ checkOffsetAndCount(data.size.toLong(), offset.toLong(), byteCount.toLong())
+
+ if (buffer.size == 0L) {
+ val count = source.read(buffer, Segment.SIZE.toLong())
+ if (count == -1L) return -1
+ }
+
+ return buffer.read(data, offset, byteCount)
+ }
+
+ override fun available(): Int {
+ if (closed) throw IOException("closed")
+ return minOf(buffer.size, Integer.MAX_VALUE).toInt()
+ }
+
+ override fun close() = this@RealBufferedSource.close()
+
+ override fun toString() = "${this@RealBufferedSource}.inputStream()"
+ }
+ }
+
+ override fun isOpen() = !closed
+
+ override fun close(): Unit = commonClose()
+ override fun timeout(): Timeout = commonTimeout()
+ override fun toString(): String = commonToString()
+}
diff --git a/okio/src/jvmMain/kotlin/okio/SegmentPool.kt b/okio/src/jvmMain/kotlin/okio/SegmentPool.kt
new file mode 100644
index 00000000..7a7b0492
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/SegmentPool.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.SegmentPool.LOCK
+import okio.SegmentPool.recycle
+import okio.SegmentPool.take
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * This class pools segments in a lock-free singly-linked stack. Though this code is lock-free it
+ * does use a sentinel [LOCK] value to defend against races. Conflicted operations are not retried,
+ * so there is no chance of blocking despite the term "lock".
+ *
+ * On [take], a caller swaps the stack's next pointer with the [LOCK] sentinel. If the stack was
+ * not already locked, the caller replaces the head node with its successor.
+ *
+ * On [recycle], a caller swaps the head with a new node whose successor is the replaced head.
+ *
+ * On conflict, operations succeed, but segments are not pushed into the stack. For example, a
+ * [take] that loses a race allocates a new segment regardless of the pool size. A [recycle] call
+ * that loses a race will not increase the size of the pool. Under significant contention, this pool
+ * will have fewer hits and the VM will do more GC and zero filling of arrays.
+ *
+ * This tracks the number of bytes in each linked list in its [Segment.limit] property. Each element
+ * has a limit that's one segment size greater than its successor element. The maximum size of the
+ * pool is a product of [MAX_SIZE] and [HASH_BUCKET_COUNT].
+ */
+internal actual object SegmentPool {
+ /** The maximum number of bytes to pool per hash bucket. */
+ // TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments?
+ actual val MAX_SIZE = 64 * 1024 // 64 KiB.
+
+ /** A sentinel segment to indicate that the linked list is currently being modified. */
+ private val LOCK = Segment(ByteArray(0), pos = 0, limit = 0, shared = false, owner = false)
+
+ /**
+ * The number of hash buckets. This number needs to balance keeping the pool small and contention
+ * low. We use the number of processors rounded up to the nearest power of two. For example a
+ * machine with 6 cores will have 8 hash buckets.
+ */
+ private val HASH_BUCKET_COUNT =
+ Integer.highestOneBit(Runtime.getRuntime().availableProcessors() * 2 - 1)
+
+ /**
+ * Hash buckets each contain a singly-linked list of segments. The index/key is a hash function of
+ * thread ID because it may reduce contention or increase locality.
+ *
+ * We don't use [ThreadLocal] because we don't know how many threads the host process has and we
+ * don't want to leak memory for the duration of a thread's life.
+ */
+ private val hashBuckets: Array<AtomicReference<Segment?>> = Array(HASH_BUCKET_COUNT) {
+ AtomicReference<Segment?>() // null value implies an empty bucket
+ }
+
+ actual val byteCount: Int
+ get() {
+ val first = firstRef().get() ?: return 0
+ return first.limit
+ }
+
+ @JvmStatic
+ actual fun take(): Segment {
+ val firstRef = firstRef()
+
+ val first = firstRef.getAndSet(LOCK)
+ when {
+ first === LOCK -> {
+ // We didn't acquire the lock. Don't take a pooled segment.
+ return Segment()
+ }
+ first == null -> {
+ // We acquired the lock but the pool was empty. Unlock and return a new segment.
+ firstRef.set(null)
+ return Segment()
+ }
+ else -> {
+ // We acquired the lock and the pool was not empty. Pop the first element and return it.
+ firstRef.set(first.next)
+ first.next = null
+ first.limit = 0
+ return first
+ }
+ }
+ }
+
+ @JvmStatic
+ actual fun recycle(segment: Segment) {
+ require(segment.next == null && segment.prev == null)
+ if (segment.shared) return // This segment cannot be recycled.
+
+ val firstRef = firstRef()
+
+ val first = firstRef.get()
+ if (first === LOCK) return // A take() is currently in progress.
+ val firstLimit = first?.limit ?: 0
+ if (firstLimit >= MAX_SIZE) return // Pool is full.
+
+ segment.next = first
+ segment.pos = 0
+ segment.limit = firstLimit + Segment.SIZE
+
+ // If we lost a race with another operation, don't recycle this segment.
+ if (!firstRef.compareAndSet(first, segment)) {
+ segment.next = null // Don't leak a reference in the pool either!
+ }
+ }
+
+ private fun firstRef(): AtomicReference<Segment?> {
+ // Get a value in [0..HASH_BUCKET_COUNT) based on the current thread.
+ val hashBucket = (Thread.currentThread().id and (HASH_BUCKET_COUNT - 1L)).toInt()
+ return hashBuckets[hashBucket]
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/SegmentedByteString.kt b/okio/src/jvmMain/kotlin/okio/SegmentedByteString.kt
new file mode 100644
index 00000000..bce9d5a9
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/SegmentedByteString.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.commonEquals
+import okio.internal.commonGetSize
+import okio.internal.commonHashCode
+import okio.internal.commonInternalGet
+import okio.internal.commonRangeEquals
+import okio.internal.commonSubstring
+import okio.internal.commonToByteArray
+import okio.internal.commonWrite
+import okio.internal.forEachSegment
+import java.io.IOException
+import java.io.OutputStream
+import java.nio.ByteBuffer
+import java.nio.charset.Charset
+import java.security.InvalidKeyException
+import java.security.MessageDigest
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+
+internal actual class SegmentedByteString internal actual constructor(
+ @Transient internal actual val segments: Array<ByteArray>,
+ @Transient internal actual val directory: IntArray
+) : ByteString(EMPTY.data) {
+
+ override fun string(charset: Charset) = toByteString().string(charset)
+
+ override fun base64() = toByteString().base64()
+
+ override fun hex() = toByteString().hex()
+
+ override fun toAsciiLowercase() = toByteString().toAsciiLowercase()
+
+ override fun toAsciiUppercase() = toByteString().toAsciiUppercase()
+
+ override fun digest(algorithm: String): ByteString {
+ val digestBytes = MessageDigest.getInstance(algorithm).run {
+ forEachSegment { data, offset, byteCount ->
+ update(data, offset, byteCount)
+ }
+ digest()
+ }
+ return ByteString(digestBytes)
+ }
+
+ override fun hmac(algorithm: String, key: ByteString): ByteString {
+ try {
+ val mac = Mac.getInstance(algorithm)
+ mac.init(SecretKeySpec(key.toByteArray(), algorithm))
+ forEachSegment { data, offset, byteCount ->
+ mac.update(data, offset, byteCount)
+ }
+ return ByteString(mac.doFinal())
+ } catch (e: InvalidKeyException) {
+ throw IllegalArgumentException(e)
+ }
+ }
+
+ override fun base64Url() = toByteString().base64Url()
+
+ override fun substring(beginIndex: Int, endIndex: Int): ByteString =
+ commonSubstring(beginIndex, endIndex)
+
+ override fun internalGet(pos: Int): Byte = commonInternalGet(pos)
+
+ override fun getSize() = commonGetSize()
+
+ override fun toByteArray(): ByteArray = commonToByteArray()
+
+ override fun asByteBuffer(): ByteBuffer = ByteBuffer.wrap(toByteArray()).asReadOnlyBuffer()
+
+ @Throws(IOException::class)
+ override fun write(out: OutputStream) {
+ forEachSegment { data, offset, byteCount ->
+ out.write(data, offset, byteCount)
+ }
+ }
+
+ override fun write(buffer: Buffer, offset: Int, byteCount: Int): Unit =
+ commonWrite(buffer, offset, byteCount)
+
+ override fun rangeEquals(
+ offset: Int,
+ other: ByteString,
+ otherOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount)
+
+ override fun rangeEquals(
+ offset: Int,
+ other: ByteArray,
+ otherOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount)
+
+ override fun indexOf(other: ByteArray, fromIndex: Int) = toByteString().indexOf(other, fromIndex)
+
+ override fun lastIndexOf(other: ByteArray, fromIndex: Int) = toByteString().lastIndexOf(
+ other,
+ fromIndex
+ )
+
+ /** Returns a copy as a non-segmented byte string. */
+ private fun toByteString() = ByteString(toByteArray())
+
+ override fun internalArray() = toByteArray()
+
+ override fun equals(other: Any?): Boolean = commonEquals(other)
+
+ override fun hashCode(): Int = commonHashCode()
+
+ override fun toString() = toByteString().toString()
+
+ @Suppress("unused", "PLATFORM_CLASS_MAPPED_TO_KOTLIN") // For Java Serialization.
+ private fun writeReplace(): Object = toByteString() as Object
+}
diff --git a/okio/src/jvmMain/kotlin/okio/Sink.kt b/okio/src/jvmMain/kotlin/okio/Sink.kt
new file mode 100644
index 00000000..e93ffb52
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/Sink.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.Closeable
+import java.io.Flushable
+import java.io.IOException
+
+actual interface Sink : Closeable, Flushable {
+ @Throws(IOException::class)
+ actual fun write(source: Buffer, byteCount: Long)
+
+ @Throws(IOException::class)
+ actual override fun flush()
+
+ actual fun timeout(): Timeout
+
+ @Throws(IOException::class)
+ actual override fun close()
+}
diff --git a/okio/src/jvmMain/kotlin/okio/Throttler.kt b/okio/src/jvmMain/kotlin/okio/Throttler.kt
new file mode 100644
index 00000000..dbb83fe3
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/Throttler.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import java.io.InterruptedIOException
+
+/**
+ * Enables limiting of Source and Sink throughput. Attach to this throttler via [source] and [sink]
+ * and set the desired throughput via [bytesPerSecond]. Multiple Sources and Sinks can be
+ * attached to a single Throttler and they will be throttled as a group, where their combined
+ * throughput will not exceed the desired throughput. The same Source or Sink can be attached to
+ * multiple Throttlers and its throughput will not exceed the desired throughput of any of the
+ * Throttlers.
+ *
+ * This class has these tuning parameters:
+ *
+ * * `bytesPerSecond`: Maximum sustained throughput. Use 0 for no limit.
+ * * `waitByteCount`: When the requested byte count is greater than this many bytes and isn't
+ * immediately available, only wait until we can allocate at least this many bytes. Use this to
+ * set the ideal byte count during sustained throughput.
+ * * `maxByteCount`: Maximum number of bytes to allocate on any call. This is also the number of
+ * bytes that will be returned before any waiting.
+ */
+class Throttler internal constructor(
+ /**
+ * The nanoTime that we've consumed all bytes through. This is never greater than the current
+ * nanoTime plus nanosForMaxByteCount.
+ */
+ private var allocatedUntil: Long
+) {
+ private var bytesPerSecond: Long = 0L
+ private var waitByteCount: Long = 8 * 1024 // 8 KiB.
+ private var maxByteCount: Long = 256 * 1024 // 256 KiB.
+
+ constructor() : this(allocatedUntil = System.nanoTime())
+
+ /** Sets the rate at which bytes will be allocated. Use 0 for no limit. */
+ @JvmOverloads
+ fun bytesPerSecond(
+ bytesPerSecond: Long,
+ waitByteCount: Long = this.waitByteCount,
+ maxByteCount: Long = this.maxByteCount
+ ) {
+ synchronized(this) {
+ require(bytesPerSecond >= 0)
+ require(waitByteCount > 0)
+ require(maxByteCount >= waitByteCount)
+
+ this.bytesPerSecond = bytesPerSecond
+ this.waitByteCount = waitByteCount
+ this.maxByteCount = maxByteCount
+ (this as Object).notifyAll()
+ }
+ }
+
+ /**
+ * Take up to `byteCount` bytes, waiting if necessary. Returns the number of bytes that were
+ * taken.
+ */
+ internal fun take(byteCount: Long): Long {
+ require(byteCount > 0)
+
+ synchronized(this) {
+ while (true) {
+ val now = System.nanoTime()
+ val byteCountOrWaitNanos = byteCountOrWaitNanos(now, byteCount)
+ if (byteCountOrWaitNanos >= 0) return byteCountOrWaitNanos
+ waitNanos(-byteCountOrWaitNanos)
+ }
+ }
+ throw AssertionError() // Unreachable, but synchronized() doesn't know that.
+ }
+
+ /**
+ * Returns the byte count to take immediately or -1 times the number of nanos to wait until the
+ * next attempt. If the returned value is negative it should be interpreted as a duration in
+ * nanos; if it is positive it should be interpreted as a byte count.
+ */
+ internal fun byteCountOrWaitNanos(now: Long, byteCount: Long): Long {
+ if (bytesPerSecond == 0L) return byteCount // No limits.
+
+ val idleInNanos = maxOf(allocatedUntil - now, 0L)
+ val immediateBytes = maxByteCount - idleInNanos.nanosToBytes()
+
+ // Fulfill the entire request without waiting.
+ if (immediateBytes >= byteCount) {
+ allocatedUntil = now + idleInNanos + byteCount.bytesToNanos()
+ return byteCount
+ }
+
+ // Fulfill a big-enough block without waiting.
+ if (immediateBytes >= waitByteCount) {
+ allocatedUntil = now + maxByteCount.bytesToNanos()
+ return immediateBytes
+ }
+
+ // Looks like we'll need to wait until we can take the minimum required bytes.
+ val minByteCount = minOf(waitByteCount, byteCount)
+ val minWaitNanos = idleInNanos + (minByteCount - maxByteCount).bytesToNanos()
+
+ // But if the wait duration truncates to zero nanos after division, don't wait.
+ if (minWaitNanos == 0L) {
+ allocatedUntil = now + maxByteCount.bytesToNanos()
+ return minByteCount
+ }
+
+ return -minWaitNanos
+ }
+
+ private fun Long.nanosToBytes() = this * bytesPerSecond / 1_000_000_000L
+
+ private fun Long.bytesToNanos() = this * 1_000_000_000L / bytesPerSecond
+
+ private fun waitNanos(nanosToWait: Long) {
+ val millisToWait = nanosToWait / 1_000_000L
+ val remainderNanos = nanosToWait - (millisToWait * 1_000_000L)
+ (this as Object).wait(millisToWait, remainderNanos.toInt())
+ }
+
+ /** Create a Source which honors this Throttler. */
+ fun source(source: Source): Source {
+ return object : ForwardingSource(source) {
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ try {
+ val toRead = take(byteCount)
+ return super.read(sink, toRead)
+ } catch (e: InterruptedException) {
+ Thread.currentThread().interrupt()
+ throw InterruptedIOException("interrupted")
+ }
+ }
+ }
+ }
+
+ /** Create a Sink which honors this Throttler. */
+ fun sink(sink: Sink): Sink {
+ return object : ForwardingSink(sink) {
+ @Throws(IOException::class)
+ override fun write(source: Buffer, byteCount: Long) {
+ try {
+ var remaining = byteCount
+ while (remaining > 0L) {
+ val toWrite = take(remaining)
+ super.write(source, toWrite)
+ remaining -= toWrite
+ }
+ } catch (e: InterruptedException) {
+ Thread.currentThread().interrupt()
+ throw InterruptedIOException("interrupted")
+ }
+ }
+ }
+ }
+}
diff --git a/okio/src/jvmMain/kotlin/okio/Timeout.kt b/okio/src/jvmMain/kotlin/okio/Timeout.kt
new file mode 100644
index 00000000..c522380f
--- /dev/null
+++ b/okio/src/jvmMain/kotlin/okio/Timeout.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import java.io.IOException
+import java.io.InterruptedIOException
+import java.util.concurrent.TimeUnit
+
+actual open class Timeout {
+ /**
+ * True if `deadlineNanoTime` is defined. There is no equivalent to null or 0 for
+ * [System.nanoTime].
+ */
+ private var hasDeadline = false
+ private var deadlineNanoTime = 0L
+ private var timeoutNanos = 0L
+
+ /**
+ * Wait at most `timeout` time before aborting an operation. Using a per-operation timeout means
+ * that as long as forward progress is being made, no sequence of operations will fail.
+ *
+ * If `timeout == 0`, operations will run indefinitely. (Operating system timeouts may still
+ * apply.)
+ */
+ open fun timeout(timeout: Long, unit: TimeUnit): Timeout {
+ require(timeout >= 0) { "timeout < 0: $timeout" }
+ timeoutNanos = unit.toNanos(timeout)
+ return this
+ }
+
+ /** Returns the timeout in nanoseconds, or `0` for no timeout. */
+ open fun timeoutNanos(): Long = timeoutNanos
+
+ /** Returns true if a deadline is enabled. */
+ open fun hasDeadline(): Boolean = hasDeadline
+
+ /**
+ * Returns the [nano time][System.nanoTime] when the deadline will be reached.
+ *
+ * @throws IllegalStateException if no deadline is set.
+ */
+ open fun deadlineNanoTime(): Long {
+ check(hasDeadline) { "No deadline" }
+ return deadlineNanoTime
+ }
+
+ /**
+ * Sets the [nano time][System.nanoTime] when the deadline will be reached. All operations must
+ * complete before this time. Use a deadline to set a maximum bound on the time spent on a
+ * sequence of operations.
+ */
+ open fun deadlineNanoTime(deadlineNanoTime: Long): Timeout {
+ this.hasDeadline = true
+ this.deadlineNanoTime = deadlineNanoTime
+ return this
+ }
+
+ /** Set a deadline of now plus `duration` time. */
+ fun deadline(duration: Long, unit: TimeUnit): Timeout {
+ require(duration > 0) { "duration <= 0: $duration" }
+ return deadlineNanoTime(System.nanoTime() + unit.toNanos(duration))
+ }
+
+ /** Clears the timeout. Operating system timeouts may still apply. */
+ open fun clearTimeout(): Timeout {
+ timeoutNanos = 0
+ return this
+ }
+
+ /** Clears the deadline. */
+ open fun clearDeadline(): Timeout {
+ hasDeadline = false
+ return this
+ }
+
+ /**
+ * Throws an [InterruptedIOException] if the deadline has been reached or if the current thread
+ * has been interrupted. This method doesn't detect timeouts; that should be implemented to
+ * asynchronously abort an in-progress operation.
+ */
+ @Throws(IOException::class)
+ open fun throwIfReached() {
+ if (Thread.currentThread().isInterrupted) {
+ // If the current thread has been interrupted.
+ throw InterruptedIOException("interrupted")
+ }
+
+ if (hasDeadline && deadlineNanoTime - System.nanoTime() <= 0) {
+ throw InterruptedIOException("deadline reached")
+ }
+ }
+
+ /**
+ * Waits on `monitor` until it is notified. Throws [InterruptedIOException] if either the thread
+ * is interrupted or if this timeout elapses before `monitor` is notified. The caller must be
+ * synchronized on `monitor`.
+ *
+ * Here's a sample class that uses `waitUntilNotified()` to await a specific state. Note that the
+ * call is made within a loop to avoid unnecessary waiting and to mitigate spurious notifications.
+ * ```
+ * class Dice {
+ * Random random = new Random();
+ * int latestTotal;
+ *
+ * public synchronized void roll() {
+ * latestTotal = 2 + random.nextInt(6) + random.nextInt(6);
+ * System.out.println("Rolled " + latestTotal);
+ * notifyAll();
+ * }
+ *
+ * public void rollAtFixedRate(int period, TimeUnit timeUnit) {
+ * Executors.newScheduledThreadPool(0).scheduleAtFixedRate(new Runnable() {
+ * public void run() {
+ * roll();
+ * }
+ * }, 0, period, timeUnit);
+ * }
+ *
+ * public synchronized void awaitTotal(Timeout timeout, int total)
+ * throws InterruptedIOException {
+ * while (latestTotal != total) {
+ * timeout.waitUntilNotified(this);
+ * }
+ * }
+ * }
+ * ```
+ */
+ @Throws(InterruptedIOException::class)
+ fun waitUntilNotified(monitor: Any) {
+ try {
+ val hasDeadline = hasDeadline()
+ val timeoutNanos = timeoutNanos()
+
+ if (!hasDeadline && timeoutNanos == 0L) {
+ (monitor as Object).wait() // There is no timeout: wait forever.
+ return
+ }
+
+ // Compute how long we'll wait.
+ val start = System.nanoTime()
+ val waitNanos = if (hasDeadline && timeoutNanos != 0L) {
+ val deadlineNanos = deadlineNanoTime() - start
+ minOf(timeoutNanos, deadlineNanos)
+ } else if (hasDeadline) {
+ deadlineNanoTime() - start
+ } else {
+ timeoutNanos
+ }
+
+ // Attempt to wait that long. This will break out early if the monitor is notified.
+ var elapsedNanos = 0L
+ if (waitNanos > 0L) {
+ val waitMillis = waitNanos / 1000000L
+ (monitor as Object).wait(waitMillis, (waitNanos - waitMillis * 1000000L).toInt())
+ elapsedNanos = System.nanoTime() - start
+ }
+
+ // Throw if the timeout elapsed before the monitor was notified.
+ if (elapsedNanos >= waitNanos) {
+ throw InterruptedIOException("timeout")
+ }
+ } catch (e: InterruptedException) {
+ Thread.currentThread().interrupt() // Retain interrupted status.
+ throw InterruptedIOException("interrupted")
+ }
+ }
+
+ /**
+ * Applies the minimum intersection between this timeout and `other`, run `block`, then finally
+ * rollback this timeout's values.
+ */
+ inline fun intersectWith(other: Timeout, block: () -> Unit) {
+ val originalTimeout = this.timeoutNanos()
+ this.timeout(minTimeout(other.timeoutNanos(), this.timeoutNanos()), TimeUnit.NANOSECONDS)
+
+ if (this.hasDeadline()) {
+ val originalDeadline = this.deadlineNanoTime()
+ if (other.hasDeadline()) {
+ this.deadlineNanoTime(Math.min(this.deadlineNanoTime(), other.deadlineNanoTime()))
+ }
+ try {
+ block()
+ } finally {
+ this.timeout(originalTimeout, TimeUnit.NANOSECONDS)
+ if (other.hasDeadline()) {
+ this.deadlineNanoTime(originalDeadline)
+ }
+ }
+ } else {
+ if (other.hasDeadline()) {
+ this.deadlineNanoTime(other.deadlineNanoTime())
+ }
+ try {
+ block()
+ } finally {
+ this.timeout(originalTimeout, TimeUnit.NANOSECONDS)
+ if (other.hasDeadline()) {
+ this.clearDeadline()
+ }
+ }
+ }
+ }
+
+ actual companion object {
+ @JvmField actual val NONE: Timeout = object : Timeout() {
+ override fun timeout(timeout: Long, unit: TimeUnit): Timeout = this
+
+ override fun deadlineNanoTime(deadlineNanoTime: Long): Timeout = this
+
+ override fun throwIfReached() {}
+ }
+
+ fun minTimeout(aNanos: Long, bNanos: Long) = when {
+ aNanos == 0L -> bNanos
+ bNanos == 0L -> aNanos
+ aNanos < bNanos -> aNanos
+ else -> bNanos
+ }
+ }
+}
diff --git a/okio/src/jvmMain/resources/META-INF/proguard/okio.pro b/okio/src/jvmMain/resources/META-INF/proguard/okio.pro
new file mode 100644
index 00000000..2b698343
--- /dev/null
+++ b/okio/src/jvmMain/resources/META-INF/proguard/okio.pro
@@ -0,0 +1,2 @@
+# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
+-dontwarn org.codehaus.mojo.animal_sniffer.*
diff --git a/okio/src/jvmTest/java/okio/AsyncTimeoutTest.java b/okio/src/jvmTest/java/okio/AsyncTimeoutTest.java
new file mode 100644
index 00000000..974218b1
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/AsyncTimeoutTest.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static okio.TestUtil.bufferWithRandomSegmentLayout;
+import static org.junit.Assert.*;
+
+/**
+ * This test uses four timeouts of varying durations: 250ms, 500ms, 750ms and
+ * 1000ms, named 'a', 'b', 'c' and 'd'.
+ */
+public final class AsyncTimeoutTest {
+ private final BlockingDeque<AsyncTimeout> timedOut = new LinkedBlockingDeque<>();
+ private final AsyncTimeout a = new RecordingAsyncTimeout();
+ private final AsyncTimeout b = new RecordingAsyncTimeout();
+ private final AsyncTimeout c = new RecordingAsyncTimeout();
+ private final AsyncTimeout d = new RecordingAsyncTimeout();
+
+ @Before public void setUp() throws Exception {
+ a.timeout( 250, TimeUnit.MILLISECONDS);
+ b.timeout( 500, TimeUnit.MILLISECONDS);
+ c.timeout( 750, TimeUnit.MILLISECONDS);
+ d.timeout(1000, TimeUnit.MILLISECONDS);
+ }
+
+ @Test public void zeroTimeoutIsNoTimeout() throws Exception {
+ AsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.timeout(0, TimeUnit.MILLISECONDS);
+ timeout.enter();
+ Thread.sleep(250);
+ assertFalse(timeout.exit());
+ assertTimedOut();
+ }
+
+ @Test public void singleInstanceTimedOut() throws Exception {
+ a.enter();
+ Thread.sleep(500);
+ assertTrue(a.exit());
+ assertTimedOut(a);
+ }
+
+ @Test public void singleInstanceNotTimedOut() throws Exception {
+ b.enter();
+ Thread.sleep(250);
+ b.exit();
+ assertFalse(b.exit());
+ assertTimedOut();
+ }
+
+ @Test public void instancesAddedAtEnd() throws Exception {
+ a.enter();
+ b.enter();
+ c.enter();
+ d.enter();
+ Thread.sleep(1250);
+ assertTrue(a.exit());
+ assertTrue(b.exit());
+ assertTrue(c.exit());
+ assertTrue(d.exit());
+ assertTimedOut(a, b, c, d);
+ }
+
+ @Test public void instancesAddedAtFront() throws Exception {
+ d.enter();
+ c.enter();
+ b.enter();
+ a.enter();
+ Thread.sleep(1250);
+ assertTrue(d.exit());
+ assertTrue(c.exit());
+ assertTrue(b.exit());
+ assertTrue(a.exit());
+ assertTimedOut(a, b, c, d);
+ }
+
+ @Test public void instancesRemovedAtFront() throws Exception {
+ a.enter();
+ b.enter();
+ c.enter();
+ d.enter();
+ assertFalse(a.exit());
+ assertFalse(b.exit());
+ assertFalse(c.exit());
+ assertFalse(d.exit());
+ assertTimedOut();
+ }
+
+ @Test public void instancesRemovedAtEnd() throws Exception {
+ a.enter();
+ b.enter();
+ c.enter();
+ d.enter();
+ assertFalse(d.exit());
+ assertFalse(c.exit());
+ assertFalse(b.exit());
+ assertFalse(a.exit());
+ assertTimedOut();
+ }
+
+ @Test public void doubleEnter() throws Exception {
+ a.enter();
+ try {
+ a.enter();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void reEnter() throws Exception {
+ a.timeout(10, SECONDS);
+ a.enter();
+ assertFalse(a.exit());
+ a.enter();
+ assertFalse(a.exit());
+ }
+
+ @Test public void reEnterAfterTimeout() throws Exception {
+ a.timeout(1, MILLISECONDS);
+ a.enter();
+ assertSame(a, timedOut.take());
+ assertTrue(a.exit());
+ a.enter();
+ assertFalse(a.exit());
+ }
+
+ @Test public void deadlineOnly() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.deadline(250, TimeUnit.MILLISECONDS);
+ timeout.enter();
+ Thread.sleep(500);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void deadlineBeforeTimeout() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.deadline(250, TimeUnit.MILLISECONDS);
+ timeout.timeout(750, TimeUnit.MILLISECONDS);
+ timeout.enter();
+ Thread.sleep(500);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void deadlineAfterTimeout() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ timeout.deadline(750, TimeUnit.MILLISECONDS);
+ timeout.enter();
+ Thread.sleep(500);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void deadlineStartsBeforeEnter() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.deadline(500, TimeUnit.MILLISECONDS);
+ Thread.sleep(500);
+ timeout.enter();
+ Thread.sleep(250);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void deadlineInThePast() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.deadlineNanoTime(System.nanoTime() - 1);
+ timeout.enter();
+ Thread.sleep(250);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void wrappedSinkTimesOut() throws Exception {
+ Sink sink = new ForwardingSink(new Buffer()) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Sink timeoutSink = timeout.sink(sink);
+ Buffer data = new Buffer().writeUtf8("a");
+ try {
+ timeoutSink.write(data, 1);
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ }
+
+ @Test public void wrappedSinkFlushTimesOut() throws Exception {
+ Sink sink = new ForwardingSink(new Buffer()) {
+ @Override public void flush() throws IOException {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Sink timeoutSink = timeout.sink(sink);
+ try {
+ timeoutSink.flush();
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ }
+
+ @Test public void wrappedSinkCloseTimesOut() throws Exception {
+ Sink sink = new ForwardingSink(new Buffer()) {
+ @Override public void close() throws IOException {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Sink timeoutSink = timeout.sink(sink);
+ try {
+ timeoutSink.close();
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ }
+
+ @Test public void wrappedSourceTimesOut() throws Exception {
+ Source source = new ForwardingSource(new Buffer()) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ try {
+ Thread.sleep(500);
+ return -1;
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Source timeoutSource = timeout.source(source);
+ try {
+ timeoutSource.read(new Buffer(), 0);
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ }
+
+ @Test public void wrappedSourceCloseTimesOut() throws Exception {
+ Source source = new ForwardingSource(new Buffer()) {
+ @Override public void close() throws IOException {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Source timeoutSource = timeout.source(source);
+ try {
+ timeoutSource.close();
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ }
+
+ @Test public void wrappedThrowsWithTimeout() throws Exception {
+ Sink sink = new ForwardingSink(new Buffer()) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ try {
+ Thread.sleep(500);
+ throw new IOException("exception and timeout");
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Sink timeoutSink = timeout.sink(sink);
+ Buffer data = new Buffer().writeUtf8("a");
+ try {
+ timeoutSink.write(data, 1);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("timeout", expected.getMessage());
+ assertEquals("exception and timeout", expected.getCause().getMessage());
+ }
+ }
+
+ @Test public void wrappedThrowsWithoutTimeout() throws Exception {
+ Sink sink = new ForwardingSink(new Buffer()) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ throw new IOException("no timeout occurred");
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Sink timeoutSink = timeout.sink(sink);
+ Buffer data = new Buffer().writeUtf8("a");
+ try {
+ timeoutSink.write(data, 1);
+ fail();
+ } catch (IOException expected) {
+ assertEquals("no timeout occurred", expected.getMessage());
+ }
+ }
+
+ /**
+ * We had a bug where writing a very large buffer would fail with an
+ * unexpected timeout because although the sink was making steady forward
+ * progress, doing it all as a single write caused a timeout.
+ */
+ @Ignore("Flaky")
+ @Test public void sinkSplitsLargeWrites() throws Exception {
+ byte[] data = new byte[512 * 1024];
+ Random dice = new Random(0);
+ dice.nextBytes(data);
+ final Buffer source = bufferWithRandomSegmentLayout(dice, data);
+ final Buffer target = new Buffer();
+
+ Sink sink = new ForwardingSink(new Buffer()) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ try {
+ Thread.sleep(byteCount / 500); // ~500 KiB/s.
+ target.write(source, byteCount);
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+
+ // Timeout after 250 ms of inactivity.
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Sink timeoutSink = timeout.sink(sink);
+
+ // Transmit 500 KiB of data, which should take ~1 second. But expect no timeout!
+ timeoutSink.write(source, source.size());
+
+ // The data should all have arrived.
+ assertEquals(ByteString.of(data), target.readByteString());
+ }
+
+ /** Asserts which timeouts fired, and in which order. */
+ private void assertTimedOut(Timeout... expected) {
+ assertEquals(Arrays.asList(expected), new ArrayList<Timeout>(timedOut));
+ }
+
+ class RecordingAsyncTimeout extends AsyncTimeout {
+ @Override protected void timedOut() {
+ timedOut.add(this);
+ }
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/BufferCursorTest.java b/okio/src/jvmTest/java/okio/BufferCursorTest.java
new file mode 100644
index 00000000..4cad858a
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/BufferCursorTest.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import static kotlin.text.StringsKt.repeat;
+import static okio.Buffer.UnsafeCursor;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static okio.TestUtil.deepCopy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+@RunWith(Parameterized.class)
+public final class BufferCursorTest {
+
+ @Parameters(name = "{0}")
+ public static List<Object[]> parameters() throws Exception {
+ List<Object[]> result = new ArrayList<>();
+ for (BufferFactory bufferFactory : BufferFactory.values()) {
+ result.add(new Object[] {bufferFactory});
+ }
+ return result;
+ }
+
+ @Parameter public BufferFactory bufferFactory;
+
+ @Test public void apiExample() throws Exception {
+ Buffer buffer = new Buffer();
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ cursor.resizeBuffer(1000_000);
+
+ do {
+ Arrays.fill(cursor.data, cursor.start, cursor.end, (byte) 'x');
+ } while (cursor.next() != -1);
+
+ cursor.seek(3);
+ cursor.data[cursor.start] = 'o';
+
+ cursor.seek(1);
+ cursor.data[cursor.start] = 'o';
+
+ cursor.resizeBuffer(4);
+ }
+
+ assertEquals(new Buffer().writeUtf8("xoxo"), buffer);
+ }
+
+ @Test public void accessSegmentBySegment() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ try (UnsafeCursor cursor = buffer.readUnsafe()) {
+ Buffer actual = new Buffer();
+ while (cursor.next() != -1L) {
+ actual.write(cursor.data, cursor.start, cursor.end - cursor.start);
+ }
+ assertEquals(buffer, actual);
+ }
+ }
+
+ @Test public void seekToNegativeOneSeeksBeforeFirstSegment() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ try (UnsafeCursor cursor = buffer.readUnsafe()) {
+ cursor.seek(-1L);
+ assertEquals(-1, cursor.offset);
+ assertEquals(null, cursor.data);
+ assertEquals(-1, cursor.start);
+ assertEquals(-1, cursor.end);
+
+ cursor.next();
+ assertEquals(0, cursor.offset);
+ }
+ }
+
+ @Test public void accessByteByByte() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ try (UnsafeCursor cursor = buffer.readUnsafe()) {
+ byte[] actual = new byte[(int) buffer.size()];
+ for (int i = 0; i < buffer.size(); i++) {
+ cursor.seek(i);
+ actual[i] = cursor.data[cursor.start];
+ }
+ assertEquals(ByteString.of(actual), buffer.snapshot());
+ }
+ }
+
+ @Test public void accessByteByByteReverse() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ try (UnsafeCursor cursor = buffer.readUnsafe()) {
+ byte[] actual = new byte[(int) buffer.size()];
+ for (int i = (int) (buffer.size() - 1); i >= 0; i--) {
+ cursor.seek(i);
+ actual[i] = cursor.data[cursor.start];
+ }
+ assertEquals(ByteString.of(actual), buffer.snapshot());
+ }
+ }
+
+ @Test public void accessByteByByteAlwaysResettingToZero() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ try (UnsafeCursor cursor = buffer.readUnsafe()) {
+ byte[] actual = new byte[(int) buffer.size()];
+ for (int i = 0; i < buffer.size(); i++) {
+ cursor.seek(i);
+ actual[i] = cursor.data[cursor.start];
+ cursor.seek(0L);
+ }
+ assertEquals(ByteString.of(actual), buffer.snapshot());
+ }
+ }
+
+ @Test public void segmentBySegmentNavigation() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ UnsafeCursor cursor = buffer.readUnsafe();
+ assertEquals(-1, cursor.offset);
+ try {
+ long lastOffset = cursor.offset;
+ while (cursor.next() != -1L) {
+ assertTrue(cursor.offset > lastOffset);
+ lastOffset = cursor.offset;
+ }
+ assertEquals(buffer.size(), cursor.offset);
+ assertNull(cursor.data);
+ assertEquals(-1, cursor.start);
+ assertEquals(-1, cursor.end);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Test public void seekWithinSegment() throws Exception {
+ assumeTrue(bufferFactory == BufferFactory.SMALL_SEGMENTED_BUFFER);
+ Buffer buffer = bufferFactory.newBuffer();
+ assertEquals("abcdefghijkl", buffer.clone().readUtf8());
+
+ // Seek to the 'f' in the "defg" segment.
+ try (UnsafeCursor cursor = buffer.readUnsafe()) {
+ assertEquals(2, cursor.seek(5)); // 2 for 2 bytes left in the segment: "fg".
+ assertEquals(5, cursor.offset);
+ assertEquals(2, cursor.end - cursor.start);
+ assertEquals('d', (char) cursor.data[cursor.start - 2]); // Out of bounds!
+ assertEquals('e', (char) cursor.data[cursor.start - 1]); // Out of bounds!
+ assertEquals('f', (char) cursor.data[cursor.start]);
+ assertEquals('g', (char) cursor.data[cursor.start + 1]);
+ }
+ }
+
+ @Test public void acquireAndRelease() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ UnsafeCursor cursor = new UnsafeCursor();
+
+ // Nothing initialized before acquire.
+ assertEquals(-1, cursor.offset);
+ assertNull(cursor.data);
+ assertEquals(-1, cursor.start);
+ assertEquals(-1, cursor.end);
+
+ buffer.readUnsafe(cursor);
+ cursor.close();
+
+ // Nothing initialized after close.
+ assertEquals(-1, cursor.offset);
+ assertNull(cursor.data);
+ assertEquals(-1, cursor.start);
+ assertEquals(-1, cursor.end);
+ }
+
+ @Test public void doubleAcquire() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ try (UnsafeCursor cursor = buffer.readUnsafe()) {
+ buffer.readUnsafe(cursor);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void releaseWithoutAcquire() throws Exception {
+ UnsafeCursor cursor = new UnsafeCursor();
+ try {
+ cursor.close();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void releaseAfterRelease() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ UnsafeCursor cursor = buffer.readUnsafe();
+ cursor.close();
+ try {
+ cursor.close();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void enlarge() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ long originalSize = buffer.size();
+
+ Buffer expected = deepCopy(buffer);
+ expected.writeUtf8("abc");
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ assertEquals(originalSize, cursor.resizeBuffer(originalSize + 3));
+ cursor.seek(originalSize);
+ cursor.data[cursor.start] = 'a';
+ cursor.seek(originalSize + 1);
+ cursor.data[cursor.start] = 'b';
+ cursor.seek(originalSize + 2);
+ cursor.data[cursor.start] = 'c';
+ }
+
+ assertEquals(expected, buffer);
+ }
+
+ @Test public void enlargeByManySegments() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ long originalSize = buffer.size();
+
+ Buffer expected = deepCopy(buffer);
+ expected.writeUtf8(repeat("x", 1_000_000));
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ cursor.resizeBuffer(originalSize + 1_000_000);
+ cursor.seek(originalSize);
+ do {
+ Arrays.fill(cursor.data, cursor.start, cursor.end, (byte) 'x');
+ } while (cursor.next() != -1);
+ }
+
+ assertEquals(expected, buffer);
+ }
+
+ @Test public void resizeNotAcquired() throws Exception {
+ UnsafeCursor cursor = new UnsafeCursor();
+ try {
+ cursor.resizeBuffer(10);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void expandNotAcquired() throws Exception {
+ UnsafeCursor cursor = new UnsafeCursor();
+ try {
+ cursor.expandBuffer(10);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void resizeAcquiredReadOnly() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+
+ try (UnsafeCursor cursor = buffer.readUnsafe()) {
+ cursor.resizeBuffer(10);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void expandAcquiredReadOnly() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+
+ try (UnsafeCursor cursor = buffer.readUnsafe()) {
+ cursor.expandBuffer(10);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void shrink() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ assumeTrue(buffer.size() > 3);
+ long originalSize = buffer.size();
+
+ Buffer expected = new Buffer();
+ deepCopy(buffer).copyTo(expected, 0, originalSize - 3);
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ assertEquals(originalSize, cursor.resizeBuffer(originalSize - 3));
+ }
+
+ assertEquals(expected, buffer);
+ }
+
+ @Test public void shrinkByManySegments() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ assumeTrue(buffer.size() <= 1_000_000);
+ long originalSize = buffer.size();
+
+ Buffer toShrink = new Buffer();
+ toShrink.writeUtf8(repeat("x", 1_000_000));
+ deepCopy(buffer).copyTo(toShrink, 0, originalSize);
+
+ UnsafeCursor cursor = new UnsafeCursor();
+ toShrink.readAndWriteUnsafe(cursor);
+ try {
+ cursor.resizeBuffer(originalSize);
+ } finally {
+ cursor.close();
+ }
+
+ Buffer expected = new Buffer();
+ expected.writeUtf8(repeat("x", (int) originalSize));
+ assertEquals(expected, toShrink);
+ }
+
+ @Test public void shrinkAdjustOffset() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ assumeTrue(buffer.size() > 4);
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ cursor.seek(buffer.size() - 1);
+ cursor.resizeBuffer(3);
+ assertEquals(3, cursor.offset);
+ assertEquals(null, cursor.data);
+ assertEquals(-1, cursor.start);
+ assertEquals(-1, cursor.end);
+ }
+ }
+
+ @Test public void resizeToSameSizeSeeksToEnd() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ long originalSize = buffer.size();
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ cursor.seek(buffer.size() / 2);
+ assertEquals(originalSize, buffer.size());
+ cursor.resizeBuffer(originalSize);
+ assertEquals(originalSize, buffer.size());
+ assertEquals(originalSize, cursor.offset);
+ assertNull(cursor.data);
+ assertEquals(-1, cursor.start);
+ assertEquals(-1, cursor.end);
+ }
+ }
+
+ @Test public void resizeEnlargeMovesCursorToOldSize() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ long originalSize = buffer.size();
+
+ Buffer expected = deepCopy(buffer);
+ expected.writeUtf8("a");
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ cursor.seek(buffer.size() / 2);
+ assertEquals(originalSize, buffer.size());
+ cursor.resizeBuffer(originalSize + 1);
+ assertEquals(originalSize, cursor.offset);
+ assertNotNull(cursor.data);
+ assertNotEquals(-1, cursor.start);
+ assertEquals(cursor.start + 1, cursor.end);
+ cursor.data[cursor.start] = 'a';
+ }
+
+ assertEquals(expected, buffer);
+ }
+
+ @Test public void resizeShrinkMovesCursorToEnd() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ assumeTrue(buffer.size() > 0);
+ long originalSize = buffer.size();
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ cursor.seek(buffer.size() / 2);
+ assertEquals(originalSize, buffer.size());
+ cursor.resizeBuffer(originalSize - 1);
+ assertEquals(originalSize - 1, cursor.offset);
+ assertNull(cursor.data);
+ assertEquals(-1, cursor.start);
+ assertEquals(-1, cursor.end);
+ }
+ }
+
+ @Test public void expand() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ long originalSize = buffer.size();
+
+ Buffer expected = deepCopy(buffer);
+ expected.writeUtf8("abcde");
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ cursor.expandBuffer(5);
+
+ for (int i = 0; i < 5; i++) {
+ cursor.data[cursor.start + i] = (byte) ('a' + i);
+ }
+
+ cursor.resizeBuffer(originalSize + 5);
+ }
+
+ assertEquals(expected, buffer);
+ }
+
+ @Test public void expandSameSegment() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ long originalSize = buffer.size();
+ assumeTrue(originalSize > 0);
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ cursor.seek(originalSize - 1);
+ int originalEnd = cursor.end;
+ assumeTrue(originalEnd < SEGMENT_SIZE);
+
+ long addedByteCount = cursor.expandBuffer(1);
+ assertEquals(SEGMENT_SIZE - originalEnd, addedByteCount);
+
+ assertEquals(originalSize + addedByteCount, buffer.size());
+ assertEquals(originalSize, cursor.offset);
+ assertEquals(originalEnd, cursor.start);
+ assertEquals(SEGMENT_SIZE, cursor.end);
+ }
+ }
+
+ @Test public void expandNewSegment() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ long originalSize = buffer.size();
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ long addedByteCount = cursor.expandBuffer(SEGMENT_SIZE);
+ assertEquals(SEGMENT_SIZE, addedByteCount);
+
+ assertEquals(originalSize, cursor.offset);
+ assertEquals(0, cursor.start);
+ assertEquals(SEGMENT_SIZE, cursor.end);
+ }
+ }
+
+ @Test public void expandMovesOffsetToOldSize() throws Exception {
+ Buffer buffer = bufferFactory.newBuffer();
+ long originalSize = buffer.size();
+
+ try (UnsafeCursor cursor = buffer.readAndWriteUnsafe()) {
+ cursor.seek(buffer.size() / 2);
+ assertEquals(originalSize, buffer.size());
+ long addedByteCount = cursor.expandBuffer(5);
+ assertEquals(originalSize + addedByteCount, buffer.size());
+ assertEquals(originalSize, cursor.offset);
+ }
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/BufferTest.java b/okio/src/jvmTest/java/okio/BufferTest.java
new file mode 100644
index 00000000..f5586d40
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/BufferTest.java
@@ -0,0 +1,580 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static kotlin.text.Charsets.UTF_8;
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_POOL_MAX_SIZE;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static okio.TestUtil.assertNoEmptySegments;
+import static okio.TestUtil.bufferWithRandomSegmentLayout;
+import static okio.TestUtil.segmentPoolByteCount;
+import static okio.TestUtil.segmentSizes;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests solely for the behavior of Buffer's implementation. For generic BufferedSink or
+ * BufferedSource behavior use BufferedSinkTest or BufferedSourceTest, respectively.
+ */
+public final class BufferTest {
+ @Test public void readAndWriteUtf8() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8("ab");
+ assertEquals(2, buffer.size());
+ buffer.writeUtf8("cdef");
+ assertEquals(6, buffer.size());
+ assertEquals("abcd", buffer.readUtf8(4));
+ assertEquals(2, buffer.size());
+ assertEquals("ef", buffer.readUtf8(2));
+ assertEquals(0, buffer.size());
+ try {
+ buffer.readUtf8(1);
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ /** Buffer's toString is the same as ByteString's. */
+ @Test public void bufferToString() {
+ assertEquals("[size=0]", new Buffer().toString());
+ assertEquals("[text=a\\r\\nb\\nc\\rd\\\\e]",
+ new Buffer().writeUtf8("a\r\nb\nc\rd\\e").toString());
+ assertEquals("[text=Tyrannosaur]",
+ new Buffer().writeUtf8("Tyrannosaur").toString());
+ assertEquals("[text=təˈranəˌsôr]", new Buffer()
+ .write(ByteString.decodeHex("74c999cb8872616ec999cb8c73c3b472"))
+ .toString());
+ assertEquals("[hex=0000000000000000000000000000000000000000000000000000000000000000000000000000"
+ + "0000000000000000000000000000000000000000000000000000]",
+ new Buffer().write(new byte[64]).toString());
+ }
+
+ @Test public void multipleSegmentBuffers() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8(repeat("a", 1000));
+ buffer.writeUtf8(repeat("b", 2500));
+ buffer.writeUtf8(repeat("c", 5000));
+ buffer.writeUtf8(repeat("d", 10000));
+ buffer.writeUtf8(repeat("e", 25000));
+ buffer.writeUtf8(repeat("f", 50000));
+
+ assertEquals(repeat("a", 999), buffer.readUtf8(999)); // a...a
+ assertEquals("a" + repeat("b", 2500) + "c", buffer.readUtf8(2502)); // ab...bc
+ assertEquals(repeat("c", 4998), buffer.readUtf8(4998)); // c...c
+ assertEquals("c" + repeat("d", 10000) + "e", buffer.readUtf8(10002)); // cd...de
+ assertEquals(repeat("e", 24998), buffer.readUtf8(24998)); // e...e
+ assertEquals("e" + repeat("f", 50000), buffer.readUtf8(50001)); // ef...f
+ assertEquals(0, buffer.size());
+ }
+
+ @Test public void fillAndDrainPool() throws Exception {
+ Buffer buffer = new Buffer();
+
+ // Take 2 * MAX_SIZE segments. This will drain the pool, even if other tests filled it.
+ buffer.write(new byte[(int) SEGMENT_POOL_MAX_SIZE]);
+ buffer.write(new byte[(int) SEGMENT_POOL_MAX_SIZE]);
+ assertEquals(0, segmentPoolByteCount());
+
+ // Recycle MAX_SIZE segments. They're all in the pool.
+ buffer.skip(SEGMENT_POOL_MAX_SIZE);
+ assertEquals(SEGMENT_POOL_MAX_SIZE, segmentPoolByteCount());
+
+ // Recycle MAX_SIZE more segments. The pool is full so they get garbage collected.
+ buffer.skip(SEGMENT_POOL_MAX_SIZE);
+ assertEquals(SEGMENT_POOL_MAX_SIZE, segmentPoolByteCount());
+
+ // Take MAX_SIZE segments to drain the pool.
+ buffer.write(new byte[(int) SEGMENT_POOL_MAX_SIZE]);
+ assertEquals(0, segmentPoolByteCount());
+
+ // Take MAX_SIZE more segments. The pool is drained so these will need to be allocated.
+ buffer.write(new byte[(int) SEGMENT_POOL_MAX_SIZE]);
+ assertEquals(0, segmentPoolByteCount());
+ }
+
+ @Test public void moveBytesBetweenBuffersShareSegment() throws Exception {
+ int size = (SEGMENT_SIZE / 2) - 1;
+ List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat("a", size), repeat("b", size));
+ assertEquals(asList(size * 2), segmentSizes);
+ }
+
+ @Test public void moveBytesBetweenBuffersReassignSegment() throws Exception {
+ int size = (SEGMENT_SIZE / 2) + 1;
+ List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat("a", size), repeat("b", size));
+ assertEquals(asList(size, size), segmentSizes);
+ }
+
+ @Test public void moveBytesBetweenBuffersMultipleSegments() throws Exception {
+ int size = 3 * SEGMENT_SIZE + 1;
+ List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat("a", size), repeat("b", size));
+ assertEquals(asList(SEGMENT_SIZE, SEGMENT_SIZE, SEGMENT_SIZE, 1,
+ SEGMENT_SIZE, SEGMENT_SIZE, SEGMENT_SIZE, 1), segmentSizes);
+ }
+
+ private List<Integer> moveBytesBetweenBuffers(String... contents) throws IOException {
+ StringBuilder expected = new StringBuilder();
+ Buffer buffer = new Buffer();
+ for (String s : contents) {
+ Buffer source = new Buffer();
+ source.writeUtf8(s);
+ buffer.writeAll(source);
+ expected.append(s);
+ }
+ List<Integer> segmentSizes = segmentSizes(buffer);
+ assertEquals(expected.toString(), buffer.readUtf8(expected.length()));
+ return segmentSizes;
+ }
+
+ /** The big part of source's first segment is being moved. */
+ @Test public void writeSplitSourceBufferLeft() {
+ int writeSize = SEGMENT_SIZE / 2 + 1;
+
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat("b", SEGMENT_SIZE - 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat("a", SEGMENT_SIZE * 2));
+ sink.write(source, writeSize);
+
+ assertEquals(asList(SEGMENT_SIZE - 10, writeSize), segmentSizes(sink));
+ assertEquals(asList(SEGMENT_SIZE - writeSize, SEGMENT_SIZE), segmentSizes(source));
+ }
+
+ /** The big part of source's first segment is staying put. */
+ @Test public void writeSplitSourceBufferRight() {
+ int writeSize = SEGMENT_SIZE / 2 - 1;
+
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat("b", SEGMENT_SIZE - 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat("a", SEGMENT_SIZE * 2));
+ sink.write(source, writeSize);
+
+ assertEquals(asList(SEGMENT_SIZE - 10, writeSize), segmentSizes(sink));
+ assertEquals(asList(SEGMENT_SIZE - writeSize, SEGMENT_SIZE), segmentSizes(source));
+ }
+
+ @Test public void writePrefixDoesntSplit() {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat("b", 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat("a", SEGMENT_SIZE * 2));
+ sink.write(source, 20);
+
+ assertEquals(asList(30), segmentSizes(sink));
+ assertEquals(asList(SEGMENT_SIZE - 20, SEGMENT_SIZE), segmentSizes(source));
+ assertEquals(30, sink.size());
+ assertEquals(SEGMENT_SIZE * 2 - 20, source.size());
+ }
+
+ @Test public void writePrefixDoesntSplitButRequiresCompact() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat("b", SEGMENT_SIZE - 10)); // limit = size - 10
+ sink.readUtf8(SEGMENT_SIZE - 20); // pos = size = 20
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat("a", SEGMENT_SIZE * 2));
+ sink.write(source, 20);
+
+ assertEquals(asList(30), segmentSizes(sink));
+ assertEquals(asList(SEGMENT_SIZE - 20, SEGMENT_SIZE), segmentSizes(source));
+ assertEquals(30, sink.size());
+ assertEquals(SEGMENT_SIZE * 2 - 20, source.size());
+ }
+
+ @Test public void copyToSpanningSegments() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat("a", SEGMENT_SIZE * 2));
+ source.writeUtf8(repeat("b", SEGMENT_SIZE * 2));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ source.copyTo(out, 10, SEGMENT_SIZE * 3);
+
+ assertEquals(repeat("a", SEGMENT_SIZE * 2 - 10) + repeat("b", SEGMENT_SIZE + 10),
+ out.toString());
+ assertEquals(repeat("a", SEGMENT_SIZE * 2) + repeat("b", SEGMENT_SIZE * 2),
+ source.readUtf8(SEGMENT_SIZE * 4));
+ }
+
+ @Test public void copyToStream() throws Exception {
+ Buffer buffer = new Buffer().writeUtf8("hello, world!");
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ buffer.copyTo(out);
+ String outString = new String(out.toByteArray(), UTF_8);
+ assertEquals("hello, world!", outString);
+ assertEquals("hello, world!", buffer.readUtf8());
+ }
+
+ @Test public void writeToSpanningSegments() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8(repeat("a", SEGMENT_SIZE * 2));
+ buffer.writeUtf8(repeat("b", SEGMENT_SIZE * 2));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ buffer.skip(10);
+ buffer.writeTo(out, SEGMENT_SIZE * 3);
+
+ assertEquals(repeat("a", SEGMENT_SIZE * 2 - 10) + repeat("b", SEGMENT_SIZE + 10),
+ out.toString());
+ assertEquals(repeat("b", SEGMENT_SIZE - 10), buffer.readUtf8(buffer.size()));
+ }
+
+ @Test public void writeToStream() throws Exception {
+ Buffer buffer = new Buffer().writeUtf8("hello, world!");
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ buffer.writeTo(out);
+ String outString = new String(out.toByteArray(), UTF_8);
+ assertEquals("hello, world!", outString);
+ assertEquals(0, buffer.size());
+ }
+
+ @Test public void readFromStream() throws Exception {
+ InputStream in = new ByteArrayInputStream("hello, world!".getBytes(UTF_8));
+ Buffer buffer = new Buffer();
+ buffer.readFrom(in);
+ String out = buffer.readUtf8();
+ assertEquals("hello, world!", out);
+ }
+
+ @Test public void readFromSpanningSegments() throws Exception {
+ InputStream in = new ByteArrayInputStream("hello, world!".getBytes(UTF_8));
+ Buffer buffer = new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE - 10));
+ buffer.readFrom(in);
+ String out = buffer.readUtf8();
+ assertEquals(repeat("a", SEGMENT_SIZE - 10) + "hello, world!", out);
+ }
+
+ @Test public void readFromStreamWithCount() throws Exception {
+ InputStream in = new ByteArrayInputStream("hello, world!".getBytes(UTF_8));
+ Buffer buffer = new Buffer();
+ buffer.readFrom(in, 10);
+ String out = buffer.readUtf8();
+ assertEquals("hello, wor", out);
+ }
+
+ @Test public void readFromDoesNotLeaveEmptyTailSegment() throws IOException {
+ Buffer buffer = new Buffer();
+ buffer.readFrom(new ByteArrayInputStream(new byte[SEGMENT_SIZE]));
+ assertNoEmptySegments(buffer);
+ }
+
+ @Test public void moveAllRequestedBytesWithRead() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat("a", 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat("b", 15));
+
+ assertEquals(10, source.read(sink, 10));
+ assertEquals(20, sink.size());
+ assertEquals(5, source.size());
+ assertEquals(repeat("a", 10) + repeat("b", 10), sink.readUtf8(20));
+ }
+
+ @Test public void moveFewerThanRequestedBytesWithRead() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat("a", 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat("b", 20));
+
+ assertEquals(20, source.read(sink, 25));
+ assertEquals(30, sink.size());
+ assertEquals(0, source.size());
+ assertEquals(repeat("a", 10) + repeat("b", 20), sink.readUtf8(30));
+ }
+
+ @Test public void indexOfWithOffset() {
+ Buffer buffer = new Buffer();
+ int halfSegment = SEGMENT_SIZE / 2;
+ buffer.writeUtf8(repeat("a", halfSegment));
+ buffer.writeUtf8(repeat("b", halfSegment));
+ buffer.writeUtf8(repeat("c", halfSegment));
+ buffer.writeUtf8(repeat("d", halfSegment));
+ assertEquals(0, buffer.indexOf((byte) 'a', 0));
+ assertEquals(halfSegment - 1, buffer.indexOf((byte) 'a', halfSegment - 1));
+ assertEquals(halfSegment, buffer.indexOf((byte) 'b', halfSegment - 1));
+ assertEquals(halfSegment * 2, buffer.indexOf((byte) 'c', halfSegment - 1));
+ assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment - 1));
+ assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 2));
+ assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 3));
+ assertEquals(halfSegment * 4 - 1, buffer.indexOf((byte) 'd', halfSegment * 4 - 1));
+ }
+
+ @Test public void byteAt() {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8("a");
+ buffer.writeUtf8(repeat("b", SEGMENT_SIZE));
+ buffer.writeUtf8("c");
+ assertEquals('a', buffer.getByte(0));
+ assertEquals('a', buffer.getByte(0)); // getByte doesn't mutate!
+ assertEquals('c', buffer.getByte(buffer.size() - 1));
+ assertEquals('b', buffer.getByte(buffer.size() - 2));
+ assertEquals('b', buffer.getByte(buffer.size() - 3));
+ }
+
+ @Test public void getByteOfEmptyBuffer() {
+ Buffer buffer = new Buffer();
+ try {
+ buffer.getByte(0);
+ fail();
+ } catch (IndexOutOfBoundsException expected) {
+ }
+ }
+
+ @Test public void writePrefixToEmptyBuffer() throws IOException {
+ Buffer sink = new Buffer();
+ Buffer source = new Buffer();
+ source.writeUtf8("abcd");
+ sink.write(source, 2);
+ assertEquals("ab", sink.readUtf8(2));
+ }
+
+ @Test public void cloneDoesNotObserveWritesToOriginal() {
+ Buffer original = new Buffer();
+ Buffer clone = original.clone();
+ original.writeUtf8("abc");
+ assertEquals(0, clone.size());
+ }
+
+ @Test public void cloneDoesNotObserveReadsFromOriginal() throws Exception {
+ Buffer original = new Buffer();
+ original.writeUtf8("abc");
+ Buffer clone = original.clone();
+ assertEquals("abc", original.readUtf8(3));
+ assertEquals(3, clone.size());
+ assertEquals("ab", clone.readUtf8(2));
+ }
+
+ @Test public void originalDoesNotObserveWritesToClone() {
+ Buffer original = new Buffer();
+ Buffer clone = original.clone();
+ clone.writeUtf8("abc");
+ assertEquals(0, original.size());
+ }
+
+ @Test public void originalDoesNotObserveReadsFromClone() throws Exception {
+ Buffer original = new Buffer();
+ original.writeUtf8("abc");
+ Buffer clone = original.clone();
+ assertEquals("abc", clone.readUtf8(3));
+ assertEquals(3, original.size());
+ assertEquals("ab", original.readUtf8(2));
+ }
+
+ @Test public void cloneMultipleSegments() throws Exception {
+ Buffer original = new Buffer();
+ original.writeUtf8(repeat("a", SEGMENT_SIZE * 3));
+ Buffer clone = original.clone();
+ original.writeUtf8(repeat("b", SEGMENT_SIZE * 3));
+ clone.writeUtf8(repeat("c", SEGMENT_SIZE * 3));
+
+ assertEquals(repeat("a", SEGMENT_SIZE * 3) + repeat("b", SEGMENT_SIZE * 3),
+ original.readUtf8(SEGMENT_SIZE * 6));
+ assertEquals(repeat("a", SEGMENT_SIZE * 3) + repeat("c", SEGMENT_SIZE * 3),
+ clone.readUtf8(SEGMENT_SIZE * 6));
+ }
+
+ @Test public void equalsAndHashCodeEmpty() {
+ Buffer a = new Buffer();
+ Buffer b = new Buffer();
+ assertTrue(a.equals(b));
+ assertTrue(a.hashCode() == b.hashCode());
+ }
+
+ @Test public void equalsAndHashCode() throws Exception {
+ Buffer a = new Buffer().writeUtf8("dog");
+ Buffer b = new Buffer().writeUtf8("hotdog");
+ assertFalse(a.equals(b));
+ assertFalse(a.hashCode() == b.hashCode());
+
+ b.readUtf8(3); // Leaves b containing 'dog'.
+ assertTrue(a.equals(b));
+ assertTrue(a.hashCode() == b.hashCode());
+ }
+
+ @Test public void equalsAndHashCodeSpanningSegments() throws Exception {
+ byte[] data = new byte[1024 * 1024];
+ Random dice = new Random(0);
+ dice.nextBytes(data);
+
+ Buffer a = bufferWithRandomSegmentLayout(dice, data);
+ Buffer b = bufferWithRandomSegmentLayout(dice, data);
+ assertTrue(a.equals(b));
+ assertTrue(a.hashCode() == b.hashCode());
+
+ data[data.length / 2]++; // Change a single byte.
+ Buffer c = bufferWithRandomSegmentLayout(dice, data);
+ assertFalse(a.equals(c));
+ assertFalse(a.hashCode() == c.hashCode());
+ }
+
+ @Test public void bufferInputStreamByteByByte() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("abc");
+
+ InputStream in = source.inputStream();
+ assertEquals(3, in.available());
+ assertEquals('a', in.read());
+ assertEquals('b', in.read());
+ assertEquals('c', in.read());
+ assertEquals(-1, in.read());
+ assertEquals(0, in.available());
+ }
+
+ @Test public void bufferInputStreamBulkReads() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("abc");
+
+ byte[] byteArray = new byte[4];
+
+ Arrays.fill(byteArray, (byte) -5);
+ InputStream in = source.inputStream();
+ assertEquals(3, in.read(byteArray));
+ assertEquals("[97, 98, 99, -5]", Arrays.toString(byteArray));
+
+ Arrays.fill(byteArray, (byte) -7);
+ assertEquals(-1, in.read(byteArray));
+ assertEquals("[-7, -7, -7, -7]", Arrays.toString(byteArray));
+ }
+
+ /**
+ * When writing data that's already buffered, there's no reason to page the
+ * data by segment.
+ */
+ @Test public void readAllWritesAllSegmentsAtOnce() throws Exception {
+ Buffer write1 = new Buffer().writeUtf8(""
+ + repeat("a", SEGMENT_SIZE)
+ + repeat("b", SEGMENT_SIZE)
+ + repeat("c", SEGMENT_SIZE));
+
+ Buffer source = new Buffer().writeUtf8(""
+ + repeat("a", SEGMENT_SIZE)
+ + repeat("b", SEGMENT_SIZE)
+ + repeat("c", SEGMENT_SIZE));
+
+ MockSink mockSink = new MockSink();
+
+ assertEquals(SEGMENT_SIZE * 3, source.readAll(mockSink));
+ assertEquals(0, source.size());
+ mockSink.assertLog("write(" + write1 + ", " + write1.size() + ")");
+ }
+
+ @Test public void writeAllMultipleSegments() throws Exception {
+ Buffer source = new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE * 3));
+ Buffer sink = new Buffer();
+
+ assertEquals(SEGMENT_SIZE * 3, sink.writeAll(source));
+ assertEquals(0, source.size());
+ assertEquals(repeat("a", SEGMENT_SIZE * 3), sink.readUtf8());
+ }
+
+ @Test public void copyTo() {
+ Buffer source = new Buffer();
+ source.writeUtf8("party");
+
+ Buffer target = new Buffer();
+ source.copyTo(target, 1, 3);
+
+ assertEquals("art", target.readUtf8());
+ assertEquals("party", source.readUtf8());
+ }
+
+ @Test public void copyToOnSegmentBoundary() {
+ String as = repeat("a", SEGMENT_SIZE);
+ String bs = repeat("b", SEGMENT_SIZE);
+ String cs = repeat("c", SEGMENT_SIZE);
+ String ds = repeat("d", SEGMENT_SIZE);
+
+ Buffer source = new Buffer();
+ source.writeUtf8(as);
+ source.writeUtf8(bs);
+ source.writeUtf8(cs);
+
+ Buffer target = new Buffer();
+ target.writeUtf8(ds);
+
+ source.copyTo(target, as.length(), bs.length() + cs.length());
+ assertEquals(ds + bs + cs, target.readUtf8());
+ }
+
+ @Test public void copyToOffSegmentBoundary() {
+ String as = repeat("a", SEGMENT_SIZE - 1);
+ String bs = repeat("b", SEGMENT_SIZE + 2);
+ String cs = repeat("c", SEGMENT_SIZE - 4);
+ String ds = repeat("d", SEGMENT_SIZE + 8);
+
+ Buffer source = new Buffer();
+ source.writeUtf8(as);
+ source.writeUtf8(bs);
+ source.writeUtf8(cs);
+
+ Buffer target = new Buffer();
+ target.writeUtf8(ds);
+
+ source.copyTo(target, as.length(), bs.length() + cs.length());
+ assertEquals(ds + bs + cs, target.readUtf8());
+ }
+
+ @Test public void copyToSourceAndTargetCanBeTheSame() {
+ String as = repeat("a", SEGMENT_SIZE);
+ String bs = repeat("b", SEGMENT_SIZE);
+
+ Buffer source = new Buffer();
+ source.writeUtf8(as);
+ source.writeUtf8(bs);
+
+ source.copyTo(source, 0, source.size());
+ assertEquals(as + bs + as + bs, source.readUtf8());
+ }
+
+ @Test public void copyToEmptySource() {
+ Buffer source = new Buffer();
+ Buffer target = new Buffer().writeUtf8("aaa");
+ source.copyTo(target, 0L, 0L);
+ assertEquals("", source.readUtf8());
+ assertEquals("aaa", target.readUtf8());
+ }
+
+ @Test public void copyToEmptyTarget() {
+ Buffer source = new Buffer().writeUtf8("aaa");
+ Buffer target = new Buffer();
+ source.copyTo(target, 0L, 3L);
+ assertEquals("aaa", source.readUtf8());
+ assertEquals("aaa", target.readUtf8());
+ }
+
+ @Test public void snapshotReportsAccurateSize() {
+ Buffer buf = new Buffer().write(new byte[] { 0, 1, 2, 3 });
+ assertEquals(1, buf.snapshot(1).size());
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/BufferedSinkJavaTest.java b/okio/src/jvmTest/java/okio/BufferedSinkJavaTest.java
new file mode 100644
index 00000000..357b992a
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/BufferedSinkJavaTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import org.junit.Test;
+
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests solely for the behavior of RealBufferedSink's implementation. For generic
+ * BufferedSink behavior use BufferedSinkTest.
+ */
+public final class BufferedSinkJavaTest {
+ @Test public void inputStreamCloses() throws Exception {
+ BufferedSink sink = Okio.buffer((Sink) new Buffer());
+ OutputStream out = sink.outputStream();
+ out.close();
+ try {
+ sink.writeUtf8("Hi!");
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("closed", e.getMessage());
+ }
+ }
+
+ @Test public void bufferedSinkEmitsTailWhenItIsComplete() throws IOException {
+ Buffer sink = new Buffer();
+ BufferedSink bufferedSink = Okio.buffer((Sink) sink);
+ bufferedSink.writeUtf8(repeat("a", SEGMENT_SIZE - 1));
+ assertEquals(0, sink.size());
+ bufferedSink.writeByte(0);
+ assertEquals(SEGMENT_SIZE, sink.size());
+ assertEquals(0, bufferedSink.getBuffer().size());
+ }
+
+ @Test public void bufferedSinkEmitMultipleSegments() throws IOException {
+ Buffer sink = new Buffer();
+ BufferedSink bufferedSink = Okio.buffer((Sink) sink);
+ bufferedSink.writeUtf8(repeat("a", SEGMENT_SIZE * 4 - 1));
+ assertEquals(SEGMENT_SIZE * 3, sink.size());
+ assertEquals(SEGMENT_SIZE - 1, bufferedSink.getBuffer().size());
+ }
+
+ @Test public void bufferedSinkFlush() throws IOException {
+ Buffer sink = new Buffer();
+ BufferedSink bufferedSink = Okio.buffer((Sink) sink);
+ bufferedSink.writeByte('a');
+ assertEquals(0, sink.size());
+ bufferedSink.flush();
+ assertEquals(0, bufferedSink.getBuffer().size());
+ assertEquals(1, sink.size());
+ }
+
+ @Test public void bytesEmittedToSinkWithFlush() throws Exception {
+ Buffer sink = new Buffer();
+ BufferedSink bufferedSink = Okio.buffer((Sink) sink);
+ bufferedSink.writeUtf8("abc");
+ bufferedSink.flush();
+ assertEquals(3, sink.size());
+ }
+
+ @Test public void bytesNotEmittedToSinkWithoutFlush() throws Exception {
+ Buffer sink = new Buffer();
+ BufferedSink bufferedSink = Okio.buffer((Sink) sink);
+ bufferedSink.writeUtf8("abc");
+ assertEquals(0, sink.size());
+ }
+
+ @Test public void bytesEmittedToSinkWithEmit() throws Exception {
+ Buffer sink = new Buffer();
+ BufferedSink bufferedSink = Okio.buffer((Sink) sink);
+ bufferedSink.writeUtf8("abc");
+ bufferedSink.emit();
+ assertEquals(3, sink.size());
+ }
+
+ @Test public void completeSegmentsEmitted() throws Exception {
+ Buffer sink = new Buffer();
+ BufferedSink bufferedSink = Okio.buffer((Sink) sink);
+ bufferedSink.writeUtf8(repeat("a", SEGMENT_SIZE * 3));
+ assertEquals(SEGMENT_SIZE * 3, sink.size());
+ }
+
+ @Test public void incompleteSegmentsNotEmitted() throws Exception {
+ Buffer sink = new Buffer();
+ BufferedSink bufferedSink = Okio.buffer((Sink) sink);
+ bufferedSink.writeUtf8(repeat("a", SEGMENT_SIZE * 3 - 1));
+ assertEquals(SEGMENT_SIZE * 2, sink.size());
+ }
+
+ @Test public void closeWithExceptionWhenWriting() throws IOException {
+ MockSink mockSink = new MockSink();
+ mockSink.scheduleThrow(0, new IOException());
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+ bufferedSink.writeByte('a');
+ try {
+ bufferedSink.close();
+ fail();
+ } catch (IOException expected) {
+ }
+ mockSink.assertLog("write([text=a], 1)", "close()");
+ }
+
+ @Test public void closeWithExceptionWhenClosing() throws IOException {
+ MockSink mockSink = new MockSink();
+ mockSink.scheduleThrow(1, new IOException());
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+ bufferedSink.writeByte('a');
+ try {
+ bufferedSink.close();
+ fail();
+ } catch (IOException expected) {
+ }
+ mockSink.assertLog("write([text=a], 1)", "close()");
+ }
+
+ @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException {
+ MockSink mockSink = new MockSink();
+ mockSink.scheduleThrow(0, new IOException("first"));
+ mockSink.scheduleThrow(1, new IOException("second"));
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+ bufferedSink.writeByte('a');
+ try {
+ bufferedSink.close();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("first", expected.getMessage());
+ }
+ mockSink.assertLog("write([text=a], 1)", "close()");
+ }
+
+ @Test public void operationsAfterClose() throws IOException {
+ MockSink mockSink = new MockSink();
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+ bufferedSink.writeByte('a');
+ bufferedSink.close();
+
+ // Test a sample set of methods.
+ try {
+ bufferedSink.writeByte('a');
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ try {
+ bufferedSink.write(new byte[10]);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ try {
+ bufferedSink.emitCompleteSegments();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ try {
+ bufferedSink.emit();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ try {
+ bufferedSink.flush();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ // Test a sample set of methods on the OutputStream.
+ OutputStream os = bufferedSink.outputStream();
+ try {
+ os.write('a');
+ fail();
+ } catch (IOException expected) {
+ }
+
+ try {
+ os.write(new byte[10]);
+ fail();
+ } catch (IOException expected) {
+ }
+
+ // Permitted
+ os.flush();
+ }
+
+ @Test public void writeAll() throws IOException {
+ MockSink mockSink = new MockSink();
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+
+ bufferedSink.getBuffer().writeUtf8("abc");
+ assertEquals(3, bufferedSink.writeAll(new Buffer().writeUtf8("def")));
+
+ assertEquals(6, bufferedSink.getBuffer().size());
+ assertEquals("abcdef", bufferedSink.getBuffer().readUtf8(6));
+ mockSink.assertLog(); // No writes.
+ }
+
+ @Test public void writeAllExhausted() throws IOException {
+ MockSink mockSink = new MockSink();
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+
+ assertEquals(0, bufferedSink.writeAll(new Buffer()));
+ assertEquals(0, bufferedSink.getBuffer().size());
+ mockSink.assertLog(); // No writes.
+ }
+
+ @Test public void writeAllWritesOneSegmentAtATime() throws IOException {
+ Buffer write1 = new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE));
+ Buffer write2 = new Buffer().writeUtf8(repeat("b", SEGMENT_SIZE));
+ Buffer write3 = new Buffer().writeUtf8(repeat("c", SEGMENT_SIZE));
+
+ Buffer source = new Buffer().writeUtf8(""
+ + repeat("a", SEGMENT_SIZE)
+ + repeat("b", SEGMENT_SIZE)
+ + repeat("c", SEGMENT_SIZE));
+
+ MockSink mockSink = new MockSink();
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+ assertEquals(SEGMENT_SIZE * 3, bufferedSink.writeAll(source));
+
+ mockSink.assertLog(
+ "write(" + write1 + ", " + write1.size() + ")",
+ "write(" + write2 + ", " + write2.size() + ")",
+ "write(" + write3 + ", " + write3.size() + ")");
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/BufferedSinkTest.java b/okio/src/jvmTest/java/okio/BufferedSinkTest.java
new file mode 100644
index 00000000..e0132846
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/BufferedSinkTest.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import static java.util.Arrays.asList;
+import static kotlin.text.Charsets.UTF_8;
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static okio.TestUtil.segmentSizes;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+@RunWith(Parameterized.class)
+public final class BufferedSinkTest {
+ private interface Factory {
+ Factory BUFFER = new Factory() {
+ @Override public BufferedSink create(Buffer data) {
+ return data;
+ }
+
+ @Override public String toString() {
+ return "Buffer";
+ }
+ };
+
+ Factory REAL_BUFFERED_SINK = new Factory() {
+ @Override public BufferedSink create(Buffer data) {
+ return Okio.buffer((Sink) data);
+ }
+
+ @Override public String toString() {
+ return "RealBufferedSink";
+ }
+ };
+
+ BufferedSink create(Buffer data);
+ }
+
+ @Parameters(name = "{0}")
+ public static List<Object[]> parameters() {
+ return Arrays.asList(
+ new Object[] {Factory.BUFFER},
+ new Object[] {Factory.REAL_BUFFERED_SINK});
+ }
+
+ @Parameter public Factory factory;
+ private Buffer data;
+ private BufferedSink sink;
+
+ @Before public void setUp() {
+ data = new Buffer();
+ sink = factory.create(data);
+ }
+
+ @Test public void writeNothing() throws IOException {
+ sink.writeUtf8("");
+ sink.flush();
+ assertEquals(0, data.size());
+ }
+
+ @Test public void writeBytes() throws Exception {
+ sink.writeByte(0xab);
+ sink.writeByte(0xcd);
+ sink.flush();
+ assertEquals("[hex=abcd]", data.toString());
+ }
+
+ @Test public void writeLastByteInSegment() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 1));
+ sink.writeByte(0x20);
+ sink.writeByte(0x21);
+ sink.flush();
+ assertEquals(asList(SEGMENT_SIZE, 1), segmentSizes(data));
+ assertEquals(repeat("a", SEGMENT_SIZE - 1), data.readUtf8(SEGMENT_SIZE - 1));
+ assertEquals("[text= !]", data.toString());
+ }
+
+ @Test public void writeShort() throws Exception {
+ sink.writeShort(0xabcd);
+ sink.writeShort(0x4321);
+ sink.flush();
+ assertEquals("[hex=abcd4321]", data.toString());
+ }
+
+ @Test public void writeShortLe() throws Exception {
+ sink.writeShortLe(0xcdab);
+ sink.writeShortLe(0x2143);
+ sink.flush();
+ assertEquals("[hex=abcd4321]", data.toString());
+ }
+
+ @Test public void writeInt() throws Exception {
+ sink.writeInt(0xabcdef01);
+ sink.writeInt(0x87654321);
+ sink.flush();
+ assertEquals("[hex=abcdef0187654321]", data.toString());
+ }
+
+ @Test public void writeLastIntegerInSegment() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 4));
+ sink.writeInt(0xabcdef01);
+ sink.writeInt(0x87654321);
+ sink.flush();
+ assertEquals(asList(SEGMENT_SIZE, 4), segmentSizes(data));
+ assertEquals(repeat("a", SEGMENT_SIZE - 4), data.readUtf8(SEGMENT_SIZE - 4));
+ assertEquals("[hex=abcdef0187654321]", data.toString());
+ }
+
+ @Test public void writeIntegerDoesNotQuiteFitInSegment() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 3));
+ sink.writeInt(0xabcdef01);
+ sink.writeInt(0x87654321);
+ sink.flush();
+ assertEquals(asList(SEGMENT_SIZE - 3, 8), segmentSizes(data));
+ assertEquals(repeat("a", SEGMENT_SIZE - 3), data.readUtf8(SEGMENT_SIZE - 3));
+ assertEquals("[hex=abcdef0187654321]", data.toString());
+ }
+
+ @Test public void writeIntLe() throws Exception {
+ sink.writeIntLe(0xabcdef01);
+ sink.writeIntLe(0x87654321);
+ sink.flush();
+ assertEquals("[hex=01efcdab21436587]", data.toString());
+ }
+
+ @Test public void writeLong() throws Exception {
+ sink.writeLong(0xabcdef0187654321L);
+ sink.writeLong(0xcafebabeb0b15c00L);
+ sink.flush();
+ assertEquals("[hex=abcdef0187654321cafebabeb0b15c00]", data.toString());
+ }
+
+ @Test public void writeLongLe() throws Exception {
+ sink.writeLongLe(0xabcdef0187654321L);
+ sink.writeLongLe(0xcafebabeb0b15c00L);
+ sink.flush();
+ assertEquals("[hex=2143658701efcdab005cb1b0bebafeca]", data.toString());
+ }
+
+ @Test public void writeByteString() throws IOException {
+ sink.write(ByteString.encodeUtf8("təˈranəˌsôr"));
+ sink.flush();
+ assertEquals(ByteString.decodeHex("74c999cb8872616ec999cb8c73c3b472"), data.readByteString());
+ }
+
+ @Test public void writeByteStringOffset() throws IOException {
+ sink.write(ByteString.encodeUtf8("təˈranəˌsôr"), 5, 5);
+ sink.flush();
+ assertEquals(ByteString.decodeHex("72616ec999"), data.readByteString());
+ }
+
+ @Test public void writeSegmentedByteString() throws IOException {
+ sink.write(new Buffer().write(ByteString.encodeUtf8("təˈranəˌsôr")).snapshot());
+ sink.flush();
+ assertEquals(ByteString.decodeHex("74c999cb8872616ec999cb8c73c3b472"), data.readByteString());
+ }
+
+ @Test public void writeSegmentedByteStringOffset() throws IOException {
+ sink.write(new Buffer().write(ByteString.encodeUtf8("təˈranəˌsôr")).snapshot(), 5, 5);
+ sink.flush();
+ assertEquals(ByteString.decodeHex("72616ec999"), data.readByteString());
+ }
+
+ @Test public void writeStringUtf8() throws IOException {
+ sink.writeUtf8("təˈranəˌsôr");
+ sink.flush();
+ assertEquals(ByteString.decodeHex("74c999cb8872616ec999cb8c73c3b472"), data.readByteString());
+ }
+
+ @Test public void writeSubstringUtf8() throws IOException {
+ sink.writeUtf8("təˈranəˌsôr", 3, 7);
+ sink.flush();
+ assertEquals(ByteString.decodeHex("72616ec999"), data.readByteString());
+ }
+
+ @Test public void writeStringWithCharset() throws IOException {
+ sink.writeString("təˈranəˌsôr", Charset.forName("utf-32be"));
+ sink.flush();
+ assertEquals(ByteString.decodeHex("0000007400000259000002c800000072000000610000006e00000259"
+ + "000002cc00000073000000f400000072"), data.readByteString());
+ }
+
+ @Test public void writeSubstringWithCharset() throws IOException {
+ sink.writeString("təˈranəˌsôr", 3, 7, Charset.forName("utf-32be"));
+ sink.flush();
+ assertEquals(ByteString.decodeHex("00000072000000610000006e00000259"), data.readByteString());
+ }
+
+ @Test public void writeUtf8SubstringWithCharset() throws IOException {
+ sink.writeString("təˈranəˌsôr", 3, 7, Charset.forName("utf-8"));
+ sink.flush();
+ assertEquals(ByteString.encodeUtf8("ranə"), data.readByteString());
+ }
+
+ @Test public void writeAll() throws Exception {
+ Buffer source = new Buffer().writeUtf8("abcdef");
+
+ assertEquals(6, sink.writeAll(source));
+ assertEquals(0, source.size());
+ sink.flush();
+ assertEquals("abcdef", data.readUtf8());
+ }
+
+ @Test public void writeSource() throws Exception {
+ Buffer source = new Buffer().writeUtf8("abcdef");
+
+ // Force resolution of the Source method overload.
+ sink.write((Source) source, 4);
+ sink.flush();
+ assertEquals("abcd", data.readUtf8());
+ assertEquals("ef", source.readUtf8());
+ }
+
+ @Test public void writeSourceReadsFully() throws Exception {
+ Source source = new ForwardingSource(new Buffer()) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ sink.writeUtf8("abcd");
+ return 4;
+ }
+ };
+
+ sink.write(source, 8);
+ sink.flush();
+ assertEquals("abcdabcd", data.readUtf8());
+ }
+
+ @Test public void writeSourcePropagatesEof() throws IOException {
+ Source source = new Buffer().writeUtf8("abcd");
+
+ try {
+ sink.write(source, 8);
+ fail();
+ } catch (EOFException expected) {
+ }
+
+ // Ensure that whatever was available was correctly written.
+ sink.flush();
+ assertEquals("abcd", data.readUtf8());
+ }
+
+ @Test public void writeSourceWithZeroIsNoOp() throws IOException {
+ // This test ensures that a zero byte count never calls through to read the source. It may be
+ // tied to something like a socket which will potentially block trying to read a segment when
+ // ultimately we don't want any data.
+ Source source = new ForwardingSource(new Buffer()) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ throw new AssertionError();
+ }
+ };
+ sink.write(source, 0);
+ assertEquals(0, data.size());
+ }
+
+ @Test public void writeAllExhausted() throws Exception {
+ Buffer source = new Buffer();
+ assertEquals(0, sink.writeAll(source));
+ assertEquals(0, source.size());
+ }
+
+ @Test public void closeEmitsBufferedBytes() throws IOException {
+ sink.writeByte('a');
+ sink.close();
+ assertEquals('a', data.readByte());
+ }
+
+ @Test public void outputStream() throws Exception {
+ OutputStream out = sink.outputStream();
+ out.write('a');
+ out.write(repeat("b", 9998).getBytes(UTF_8));
+ out.write('c');
+ out.flush();
+ assertEquals("a" + repeat("b", 9998) + "c", data.readUtf8());
+ }
+
+ @Test public void outputStreamBounds() throws Exception {
+ OutputStream out = sink.outputStream();
+ try {
+ out.write(new byte[100], 50, 51);
+ fail();
+ } catch (ArrayIndexOutOfBoundsException expected) {
+ }
+ }
+
+ @Test public void longDecimalString() throws IOException {
+ assertLongDecimalString(0);
+ assertLongDecimalString(Long.MIN_VALUE);
+ assertLongDecimalString(Long.MAX_VALUE);
+
+ for (int i = 1; i < 20; i++) {
+ long value = BigInteger.valueOf(10L).pow(i).longValue();
+ assertLongDecimalString(value - 1);
+ assertLongDecimalString(value);
+ }
+ }
+
+ private void assertLongDecimalString(long value) throws IOException {
+ sink.writeDecimalLong(value).writeUtf8("zzz").flush();
+ String expected = Long.toString(value) + "zzz";
+ String actual = data.readUtf8();
+ assertEquals(value + " expected " + expected + " but was " + actual, actual, expected);
+ }
+
+ @Test public void longHexString() throws IOException {
+ assertLongHexString(0);
+ assertLongHexString(Long.MIN_VALUE);
+ assertLongHexString(Long.MAX_VALUE);
+
+ for (int i = 0; i < 63; i++) {
+ assertLongHexString((1L << i) - 1);
+ assertLongHexString(1L << i);
+ }
+ }
+
+ @Test public void writeNioBuffer() throws Exception {
+ String expected = "abcdefg";
+
+ ByteBuffer nioByteBuffer = ByteBuffer.allocate(1024);
+ nioByteBuffer.put("abcdefg".getBytes(UTF_8));
+ nioByteBuffer.flip();
+
+ int byteCount = sink.write(nioByteBuffer);
+ assertEquals(expected.length(), byteCount);
+ assertEquals(expected.length(), nioByteBuffer.position());
+ assertEquals(expected.length(), nioByteBuffer.limit());
+
+ sink.flush();
+ assertEquals(expected, data.readUtf8());
+ }
+
+ @Test public void writeLargeNioBufferWritesAllData() throws Exception {
+ String expected = repeat("a", SEGMENT_SIZE * 3);
+
+ ByteBuffer nioByteBuffer = ByteBuffer.allocate(SEGMENT_SIZE * 4);
+ nioByteBuffer.put(repeat("a", SEGMENT_SIZE * 3).getBytes(UTF_8));
+ nioByteBuffer.flip();
+
+ int byteCount = sink.write(nioByteBuffer);
+ assertEquals(expected.length(), byteCount);
+ assertEquals(expected.length(), nioByteBuffer.position());
+ assertEquals(expected.length(), nioByteBuffer.limit());
+
+ sink.flush();
+ assertEquals(expected, data.readUtf8());
+ }
+
+ private void assertLongHexString(long value) throws IOException {
+ sink.writeHexadecimalUnsignedLong(value).writeUtf8("zzz").flush();
+ String expected = String.format("%x", value) + "zzz";
+ String actual = data.readUtf8();
+ assertEquals(value + " expected " + expected + " but was " + actual, actual, expected);
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/BufferedSourceJavaTest.java b/okio/src/jvmTest/java/okio/BufferedSourceJavaTest.java
new file mode 100644
index 00000000..470a2ddc
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/BufferedSourceJavaTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import org.junit.Test;
+
+import static kotlin.text.Charsets.UTF_8;
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests solely for the behavior of RealBufferedSource's implementation. For generic
+ * BufferedSource behavior use BufferedSourceTest.
+ */
+public final class BufferedSourceJavaTest {
+ @Test public void inputStreamTracksSegments() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("a");
+ source.writeUtf8(repeat("b", SEGMENT_SIZE));
+ source.writeUtf8("c");
+
+ InputStream in = Okio.buffer((Source) source).inputStream();
+ assertEquals(0, in.available());
+ assertEquals(SEGMENT_SIZE + 2, source.size());
+
+ // Reading one byte buffers a full segment.
+ assertEquals('a', in.read());
+ assertEquals(SEGMENT_SIZE - 1, in.available());
+ assertEquals(2, source.size());
+
+ // Reading as much as possible reads the rest of that buffered segment.
+ byte[] data = new byte[SEGMENT_SIZE * 2];
+ assertEquals(SEGMENT_SIZE - 1, in.read(data, 0, data.length));
+ assertEquals(repeat("b", SEGMENT_SIZE - 1), new String(data, 0, SEGMENT_SIZE - 1, UTF_8));
+ assertEquals(2, source.size());
+
+ // Continuing to read buffers the next segment.
+ assertEquals('b', in.read());
+ assertEquals(1, in.available());
+ assertEquals(0, source.size());
+
+ // Continuing to read reads from the buffer.
+ assertEquals('c', in.read());
+ assertEquals(0, in.available());
+ assertEquals(0, source.size());
+
+ // Once we've exhausted the source, we're done.
+ assertEquals(-1, in.read());
+ assertEquals(0, source.size());
+ }
+
+ @Test public void inputStreamCloses() throws Exception {
+ BufferedSource source = Okio.buffer((Source) new Buffer());
+ InputStream in = source.inputStream();
+ in.close();
+ try {
+ source.require(1);
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("closed", e.getMessage());
+ }
+ }
+
+ @Test public void indexOfStopsReadingAtLimit() throws Exception {
+ Buffer buffer = new Buffer().writeUtf8("abcdef");
+ BufferedSource bufferedSource = Okio.buffer(new ForwardingSource(buffer) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ return super.read(sink, Math.min(1, byteCount));
+ }
+ });
+
+ assertEquals(6, buffer.size());
+ assertEquals(-1, bufferedSource.indexOf((byte) 'e', 0, 4));
+ assertEquals(2, buffer.size());
+ }
+
+ @Test public void requireTracksBufferFirst() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("bb");
+
+ BufferedSource bufferedSource = Okio.buffer((Source) source);
+ bufferedSource.getBuffer().writeUtf8("aa");
+
+ bufferedSource.require(2);
+ assertEquals(2, bufferedSource.getBuffer().size());
+ assertEquals(2, source.size());
+ }
+
+ @Test public void requireIncludesBufferBytes() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("b");
+
+ BufferedSource bufferedSource = Okio.buffer((Source) source);
+ bufferedSource.getBuffer().writeUtf8("a");
+
+ bufferedSource.require(2);
+ assertEquals("ab", bufferedSource.getBuffer().readUtf8(2));
+ }
+
+ @Test public void requireInsufficientData() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("a");
+
+ BufferedSource bufferedSource = Okio.buffer((Source) source);
+
+ try {
+ bufferedSource.require(2);
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void requireReadsOneSegmentAtATime() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat("a", SEGMENT_SIZE));
+ source.writeUtf8(repeat("b", SEGMENT_SIZE));
+
+ BufferedSource bufferedSource = Okio.buffer((Source) source);
+
+ bufferedSource.require(2);
+ assertEquals(SEGMENT_SIZE, source.size());
+ assertEquals(SEGMENT_SIZE, bufferedSource.getBuffer().size());
+ }
+
+ @Test public void skipReadsOneSegmentAtATime() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat("a", SEGMENT_SIZE));
+ source.writeUtf8(repeat("b", SEGMENT_SIZE));
+ BufferedSource bufferedSource = Okio.buffer((Source) source);
+ bufferedSource.skip(2);
+ assertEquals(SEGMENT_SIZE, source.size());
+ assertEquals(SEGMENT_SIZE - 2, bufferedSource.getBuffer().size());
+ }
+
+ @Test public void skipTracksBufferFirst() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("bb");
+
+ BufferedSource bufferedSource = Okio.buffer((Source) source);
+ bufferedSource.getBuffer().writeUtf8("aa");
+
+ bufferedSource.skip(2);
+ assertEquals(0, bufferedSource.getBuffer().size());
+ assertEquals(2, source.size());
+ }
+
+ @Test public void operationsAfterClose() throws IOException {
+ Buffer source = new Buffer();
+ BufferedSource bufferedSource = Okio.buffer((Source) source);
+ bufferedSource.close();
+
+ // Test a sample set of methods.
+ try {
+ bufferedSource.indexOf((byte) 1);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ try {
+ bufferedSource.skip(1);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ try {
+ bufferedSource.readByte();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ try {
+ bufferedSource.readByteString(10);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ // Test a sample set of methods on the InputStream.
+ InputStream is = bufferedSource.inputStream();
+ try {
+ is.read();
+ fail();
+ } catch (IOException expected) {
+ }
+
+ try {
+ is.read(new byte[10]);
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ /**
+ * We don't want readAll to buffer an unbounded amount of data. Instead it
+ * should buffer a segment, write it, and repeat.
+ */
+ @Test public void readAllReadsOneSegmentAtATime() throws IOException {
+ Buffer write1 = new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE));
+ Buffer write2 = new Buffer().writeUtf8(repeat("b", SEGMENT_SIZE));
+ Buffer write3 = new Buffer().writeUtf8(repeat("c", SEGMENT_SIZE));
+
+ Buffer source = new Buffer().writeUtf8(""
+ + repeat("a", SEGMENT_SIZE)
+ + repeat("b", SEGMENT_SIZE)
+ + repeat("c", SEGMENT_SIZE));
+
+ MockSink mockSink = new MockSink();
+ BufferedSource bufferedSource = Okio.buffer((Source) source);
+ assertEquals(SEGMENT_SIZE * 3, bufferedSource.readAll(mockSink));
+ mockSink.assertLog(
+ "write(" + write1 + ", " + write1.size() + ")",
+ "write(" + write2 + ", " + write2.size() + ")",
+ "write(" + write3 + ", " + write3.size() + ")");
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/BufferedSourceTest.java b/okio/src/jvmTest/java/okio/BufferedSourceTest.java
new file mode 100644
index 00000000..149ae64a
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/BufferedSourceTest.java
@@ -0,0 +1,1492 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import static kotlin.text.Charsets.US_ASCII;
+import static kotlin.text.Charsets.UTF_8;
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static okio.TestUtil.assertByteArrayEquals;
+import static okio.TestUtil.assertByteArraysEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+@RunWith(Parameterized.class)
+public final class BufferedSourceTest {
+ interface Factory {
+ Factory BUFFER = new Factory() {
+ @Override public Pipe pipe() {
+ Buffer buffer = new Buffer();
+ Pipe result = new Pipe();
+ result.sink = buffer;
+ result.source = buffer;
+ return result;
+ }
+
+ @Override public boolean isOneByteAtATime() {
+ return false;
+ }
+
+ @Override public String toString() {
+ return "Buffer";
+ }
+ };
+
+ Factory REAL_BUFFERED_SOURCE = new Factory() {
+ @Override public Pipe pipe() {
+ Buffer buffer = new Buffer();
+ Pipe result = new Pipe();
+ result.sink = buffer;
+ result.source = Okio.buffer((Source) buffer);
+ return result;
+ }
+
+ @Override public boolean isOneByteAtATime() {
+ return false;
+ }
+
+ @Override public String toString() {
+ return "RealBufferedSource";
+ }
+ };
+
+ /**
+ * A factory deliberately written to create buffers whose internal segments are always 1 byte
+ * long. We like testing with these segments because are likely to trigger bugs!
+ */
+ Factory ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE = new Factory() {
+ @Override public Pipe pipe() {
+ Buffer buffer = new Buffer();
+ Pipe result = new Pipe();
+ result.sink = buffer;
+ result.source = Okio.buffer(new ForwardingSource(buffer) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ // Read one byte into a new buffer, then clone it so that the segment is shared.
+ // Shared segments cannot be compacted so we'll get a long chain of short segments.
+ Buffer box = new Buffer();
+ long result = super.read(box, Math.min(byteCount, 1L));
+ if (result > 0L) sink.write(box.clone(), result);
+ return result;
+ }
+ });
+ return result;
+ }
+
+ @Override public boolean isOneByteAtATime() {
+ return true;
+ }
+
+ @Override public String toString() {
+ return "OneByteAtATimeBufferedSource";
+ }
+ };
+
+ Factory ONE_BYTE_AT_A_TIME_BUFFER = new Factory() {
+ @Override public Pipe pipe() {
+ Buffer buffer = new Buffer();
+ Pipe result = new Pipe();
+ result.source = buffer;
+ result.sink = Okio.buffer(new ForwardingSink(buffer) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ // Write each byte into a new buffer, then clone it so that the segments are shared.
+ // Shared segments cannot be compacted so we'll get a long chain of short segments.
+ for (int i = 0; i < byteCount; i++) {
+ Buffer box = new Buffer();
+ box.write(source, 1);
+ super.write(box.clone(), 1);
+ }
+ }
+ });
+ return result;
+ }
+
+ @Override public boolean isOneByteAtATime() {
+ return true;
+ }
+
+ @Override public String toString() {
+ return "OneByteAtATimeBuffer";
+ }
+ };
+
+ Factory PEEK_BUFFER = new Factory() {
+ @Override public Pipe pipe() {
+ Buffer buffer = new Buffer();
+ Pipe result = new Pipe();
+ result.sink = buffer;
+ result.source = buffer.peek();
+ return result;
+ }
+
+ @Override public boolean isOneByteAtATime() {
+ return false;
+ }
+
+ @Override public String toString() {
+ return "PeekBuffer";
+ }
+ };
+
+ Factory PEEK_BUFFERED_SOURCE = new Factory() {
+ @Override public Pipe pipe() {
+ Buffer buffer = new Buffer();
+ Pipe result = new Pipe();
+ result.sink = buffer;
+ result.source = Okio.buffer((Source) buffer).peek();
+ return result;
+ }
+
+ @Override public boolean isOneByteAtATime() {
+ return false;
+ }
+
+ @Override public String toString() {
+ return "PeekBufferedSource";
+ }
+ };
+
+ Pipe pipe();
+
+ boolean isOneByteAtATime();
+ }
+
+ private static class Pipe {
+ BufferedSink sink;
+ BufferedSource source;
+ }
+
+ @Parameters(name = "{0}")
+ public static List<Object[]> parameters() {
+ return Arrays.asList(
+ new Object[] { Factory.BUFFER },
+ new Object[] { Factory.REAL_BUFFERED_SOURCE },
+ new Object[] { Factory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE },
+ new Object[] { Factory.ONE_BYTE_AT_A_TIME_BUFFER },
+ new Object[] { Factory.PEEK_BUFFER },
+ new Object[] { Factory.PEEK_BUFFERED_SOURCE });
+ }
+
+ @Parameter public Factory factory;
+ private BufferedSink sink;
+ private BufferedSource source;
+
+ @Before public void setUp() {
+ Pipe pipe = factory.pipe();
+ sink = pipe.sink;
+ source = pipe.source;
+ }
+
+ @Test public void readBytes() throws Exception {
+ sink.write(new byte[] { (byte) 0xab, (byte) 0xcd });
+ sink.emit();
+ assertEquals(0xab, source.readByte() & 0xff);
+ assertEquals(0xcd, source.readByte() & 0xff);
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readByteTooShortThrows() throws IOException {
+ try {
+ source.readByte();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void readShort() throws Exception {
+ sink.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01
+ });
+ sink.emit();
+ assertEquals((short) 0xabcd, source.readShort());
+ assertEquals((short) 0xef01, source.readShort());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readShortLe() throws Exception {
+ sink.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10
+ });
+ sink.emit();
+ assertEquals((short) 0xcdab, source.readShortLe());
+ assertEquals((short) 0x10ef, source.readShortLe());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readShortSplitAcrossMultipleSegments() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 1));
+ sink.write(new byte[] { (byte) 0xab, (byte) 0xcd });
+ sink.emit();
+ source.skip(SEGMENT_SIZE - 1);
+ assertEquals((short) 0xabcd, source.readShort());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readShortTooShortThrows() throws IOException {
+ sink.writeShort(Short.MAX_VALUE);
+ sink.emit();
+ source.readByte();
+ try {
+ source.readShort();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void readShortLeTooShortThrows() throws IOException {
+ sink.writeShortLe(Short.MAX_VALUE);
+ sink.emit();
+ source.readByte();
+ try {
+ source.readShortLe();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void readInt() throws Exception {
+ sink.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21
+ });
+ sink.emit();
+ assertEquals(0xabcdef01, source.readInt());
+ assertEquals(0x87654321, source.readInt());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readIntLe() throws Exception {
+ sink.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21
+ });
+ sink.emit();
+ assertEquals(0x10efcdab, source.readIntLe());
+ assertEquals(0x21436587, source.readIntLe());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readIntSplitAcrossMultipleSegments() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 3));
+ sink.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01
+ });
+ sink.emit();
+ source.skip(SEGMENT_SIZE - 3);
+ assertEquals(0xabcdef01, source.readInt());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readIntTooShortThrows() throws IOException {
+ sink.writeInt(Integer.MAX_VALUE);
+ sink.emit();
+ source.readByte();
+ try {
+ source.readInt();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void readIntLeTooShortThrows() throws IOException {
+ sink.writeIntLe(Integer.MAX_VALUE);
+ sink.emit();
+ source.readByte();
+ try {
+ source.readIntLe();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void readLong() throws Exception {
+ sink.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21, (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69, (byte) 0x12, (byte) 0x23,
+ (byte) 0x34, (byte) 0x45
+ });
+ sink.emit();
+ assertEquals(0xabcdef1087654321L, source.readLong());
+ assertEquals(0x3647586912233445L, source.readLong());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readLongLe() throws Exception {
+ sink.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21, (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69, (byte) 0x12, (byte) 0x23,
+ (byte) 0x34, (byte) 0x45
+ });
+ sink.emit();
+ assertEquals(0x2143658710efcdabL, source.readLongLe());
+ assertEquals(0x4534231269584736L, source.readLongLe());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readLongSplitAcrossMultipleSegments() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 7));
+ sink.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21,
+ });
+ sink.emit();
+ source.skip(SEGMENT_SIZE - 7);
+ assertEquals(0xabcdef0187654321L, source.readLong());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readLongTooShortThrows() throws IOException {
+ sink.writeLong(Long.MAX_VALUE);
+ sink.emit();
+ source.readByte();
+ try {
+ source.readLong();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void readLongLeTooShortThrows() throws IOException {
+ sink.writeLongLe(Long.MAX_VALUE);
+ sink.emit();
+ source.readByte();
+ try {
+ source.readLongLe();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void readAll() throws IOException {
+ source.getBuffer().writeUtf8("abc");
+ sink.writeUtf8("def");
+ sink.emit();
+
+ Buffer sink = new Buffer();
+ assertEquals(6, source.readAll(sink));
+ assertEquals("abcdef", sink.readUtf8());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readAllExhausted() throws IOException {
+ MockSink mockSink = new MockSink();
+ assertEquals(0, source.readAll(mockSink));
+ assertTrue(source.exhausted());
+ mockSink.assertLog();
+ }
+
+ @Test public void readExhaustedSource() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat("a", 10));
+ assertEquals(-1, source.read(sink, 10));
+ assertEquals(10, sink.size());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readZeroBytesFromSource() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat("a", 10));
+
+ // Either 0 or -1 is reasonable here. For consistency with Android's
+ // ByteArrayInputStream we return 0.
+ assertEquals(-1, source.read(sink, 0));
+ assertEquals(10, sink.size());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readFully() throws Exception {
+ sink.writeUtf8(repeat("a", 10000));
+ sink.emit();
+ Buffer sink = new Buffer();
+ source.readFully(sink, 9999);
+ assertEquals(repeat("a", 9999), sink.readUtf8());
+ assertEquals("a", source.readUtf8());
+ }
+
+ @Test public void readFullyTooShortThrows() throws IOException {
+ sink.writeUtf8("Hi");
+ sink.emit();
+ Buffer sink = new Buffer();
+ try {
+ source.readFully(sink, 5);
+ fail();
+ } catch (EOFException ignored) {
+ }
+
+ // Verify we read all that we could from the source.
+ assertEquals("Hi", sink.readUtf8());
+ }
+
+ @Test public void readFullyByteArray() throws IOException {
+ Buffer data = new Buffer();
+ data.writeUtf8("Hello").writeUtf8(repeat("e", SEGMENT_SIZE));
+
+ byte[] expected = data.clone().readByteArray();
+ sink.write(data, data.size());
+ sink.emit();
+
+ byte[] sink = new byte[SEGMENT_SIZE + 5];
+ source.readFully(sink);
+ assertByteArraysEquals(expected, sink);
+ }
+
+ @Test public void readFullyByteArrayTooShortThrows() throws IOException {
+ sink.writeUtf8("Hello");
+ sink.emit();
+
+ byte[] array = new byte[6];
+ try {
+ source.readFully(array);
+ fail();
+ } catch (EOFException ignored) {
+ }
+
+ // Verify we read all that we could from the source.
+ assertByteArraysEquals(new byte[] { 'H', 'e', 'l', 'l', 'o', 0 }, array);
+ }
+
+ @Test public void readIntoByteArray() throws IOException {
+ sink.writeUtf8("abcd");
+ sink.emit();
+
+ byte[] sink = new byte[3];
+ int read = source.read(sink);
+ if (factory.isOneByteAtATime()) {
+ assertEquals(1, read);
+ byte[] expected = { 'a', 0, 0 };
+ assertByteArraysEquals(expected, sink);
+ } else {
+ assertEquals(3, read);
+ byte[] expected = { 'a', 'b', 'c' };
+ assertByteArraysEquals(expected, sink);
+ }
+ }
+
+ @Test public void readIntoByteArrayNotEnough() throws IOException {
+ sink.writeUtf8("abcd");
+ sink.emit();
+
+ byte[] sink = new byte[5];
+ int read = source.read(sink);
+ if (factory.isOneByteAtATime()) {
+ assertEquals(1, read);
+ byte[] expected = { 'a', 0, 0, 0, 0 };
+ assertByteArraysEquals(expected, sink);
+ } else {
+ assertEquals(4, read);
+ byte[] expected = { 'a', 'b', 'c', 'd', 0 };
+ assertByteArraysEquals(expected, sink);
+ }
+ }
+
+ @Test public void readIntoByteArrayOffsetAndCount() throws IOException {
+ sink.writeUtf8("abcd");
+ sink.emit();
+
+ byte[] sink = new byte[7];
+ int read = source.read(sink, 2, 3);
+ if (factory.isOneByteAtATime()) {
+ assertEquals(1, read);
+ byte[] expected = { 0, 0, 'a', 0, 0, 0, 0 };
+ assertByteArraysEquals(expected, sink);
+ } else {
+ assertEquals(3, read);
+ byte[] expected = { 0, 0, 'a', 'b', 'c', 0, 0 };
+ assertByteArraysEquals(expected, sink);
+ }
+ }
+
+ @Test public void readByteArray() throws IOException {
+ String string = "abcd" + repeat("e", SEGMENT_SIZE);
+ sink.writeUtf8(string);
+ sink.emit();
+ assertByteArraysEquals(string.getBytes(UTF_8), source.readByteArray());
+ }
+
+ @Test public void readByteArrayPartial() throws IOException {
+ sink.writeUtf8("abcd");
+ sink.emit();
+ assertEquals("[97, 98, 99]", Arrays.toString(source.readByteArray(3)));
+ assertEquals("d", source.readUtf8(1));
+ }
+
+ @Test public void readByteArrayTooShortThrows() throws IOException {
+ sink.writeUtf8("abc");
+ sink.emit();
+ try {
+ source.readByteArray(4);
+ fail();
+ } catch (EOFException expected) {
+ }
+ assertEquals("abc", source.readUtf8()); // The read shouldn't consume any data.
+ }
+
+ @Test public void readByteString() throws IOException {
+ sink.writeUtf8("abcd").writeUtf8(repeat("e", SEGMENT_SIZE));
+ sink.emit();
+ assertEquals("abcd" + repeat("e", SEGMENT_SIZE), source.readByteString().utf8());
+ }
+
+ @Test public void readByteStringPartial() throws IOException {
+ sink.writeUtf8("abcd").writeUtf8(repeat("e", SEGMENT_SIZE));
+ sink.emit();
+ assertEquals("abc", source.readByteString(3).utf8());
+ assertEquals("d", source.readUtf8(1));
+ }
+
+ @Test public void readByteStringTooShortThrows() throws IOException {
+ sink.writeUtf8("abc");
+ sink.emit();
+ try {
+ source.readByteString(4);
+ fail();
+ } catch (EOFException expected) {
+ }
+ assertEquals("abc", source.readUtf8()); // The read shouldn't consume any data.
+ }
+
+ @Test public void readSpecificCharsetPartial() throws Exception {
+ sink.write(ByteString.decodeHex("0000007600000259000002c80000006c000000e40000007300000259"
+ + "000002cc000000720000006100000070000000740000025900000072"));
+ sink.emit();
+ assertEquals("vəˈläsə", source.readString(7 * 4, Charset.forName("utf-32")));
+ }
+
+ @Test public void readSpecificCharset() throws Exception {
+ sink.write(ByteString.decodeHex("0000007600000259000002c80000006c000000e40000007300000259"
+ + "000002cc000000720000006100000070000000740000025900000072"));
+ sink.emit();
+ assertEquals("vəˈläsəˌraptər", source.readString(Charset.forName("utf-32")));
+ }
+
+ @Test public void readStringTooShortThrows() throws IOException {
+ sink.writeString("abc", US_ASCII);
+ sink.emit();
+ try {
+ source.readString(4, US_ASCII);
+ fail();
+ } catch (EOFException expected) {
+ }
+ assertEquals("abc", source.readUtf8()); // The read shouldn't consume any data.
+ }
+
+ @Test public void readUtf8SpansSegments() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE * 2));
+ sink.emit();
+ source.skip(SEGMENT_SIZE - 1);
+ assertEquals("aa", source.readUtf8(2));
+ }
+
+ @Test public void readUtf8Segment() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE));
+ sink.emit();
+ assertEquals(repeat("a", SEGMENT_SIZE), source.readUtf8(SEGMENT_SIZE));
+ }
+
+ @Test public void readUtf8PartialBuffer() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE + 20));
+ sink.emit();
+ assertEquals(repeat("a", SEGMENT_SIZE + 10), source.readUtf8(SEGMENT_SIZE + 10));
+ }
+
+ @Test public void readUtf8EntireBuffer() throws Exception {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE * 2));
+ sink.emit();
+ assertEquals(repeat("a", SEGMENT_SIZE * 2), source.readUtf8());
+ }
+
+ @Test public void readUtf8TooShortThrows() throws IOException {
+ sink.writeUtf8("abc");
+ sink.emit();
+ try {
+ source.readUtf8(4L);
+ fail();
+ } catch (EOFException expected) {
+ }
+ assertEquals("abc", source.readUtf8()); // The read shouldn't consume any data.
+ }
+
+ @Test public void skip() throws Exception {
+ sink.writeUtf8("a");
+ sink.writeUtf8(repeat("b", SEGMENT_SIZE));
+ sink.writeUtf8("c");
+ sink.emit();
+ source.skip(1);
+ assertEquals('b', source.readByte() & 0xff);
+ source.skip(SEGMENT_SIZE - 2);
+ assertEquals('b', source.readByte() & 0xff);
+ source.skip(1);
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void skipInsufficientData() throws Exception {
+ sink.writeUtf8("a");
+ sink.emit();
+
+ try {
+ source.skip(2);
+ fail();
+ } catch (EOFException ignored) {
+ }
+ }
+
+ @Test public void indexOf() throws Exception {
+ // The segment is empty.
+ assertEquals(-1, source.indexOf((byte) 'a'));
+
+ // The segment has one value.
+ sink.writeUtf8("a"); // a
+ sink.emit();
+ assertEquals(0, source.indexOf((byte) 'a'));
+ assertEquals(-1, source.indexOf((byte) 'b'));
+
+ // The segment has lots of data.
+ sink.writeUtf8(repeat("b", SEGMENT_SIZE - 2)); // ab...b
+ sink.emit();
+ assertEquals(0, source.indexOf((byte) 'a'));
+ assertEquals(1, source.indexOf((byte) 'b'));
+ assertEquals(-1, source.indexOf((byte) 'c'));
+
+ // The segment doesn't start at 0, it starts at 2.
+ source.skip(2); // b...b
+ assertEquals(-1, source.indexOf((byte) 'a'));
+ assertEquals(0, source.indexOf((byte) 'b'));
+ assertEquals(-1, source.indexOf((byte) 'c'));
+
+ // The segment is full.
+ sink.writeUtf8("c"); // b...bc
+ sink.emit();
+ assertEquals(-1, source.indexOf((byte) 'a'));
+ assertEquals(0, source.indexOf((byte) 'b'));
+ assertEquals(SEGMENT_SIZE - 3, source.indexOf((byte) 'c'));
+
+ // The segment doesn't start at 2, it starts at 4.
+ source.skip(2); // b...bc
+ assertEquals(-1, source.indexOf((byte) 'a'));
+ assertEquals(0, source.indexOf((byte) 'b'));
+ assertEquals(SEGMENT_SIZE - 5, source.indexOf((byte) 'c'));
+
+ // Two segments.
+ sink.writeUtf8("d"); // b...bcd, d is in the 2nd segment.
+ sink.emit();
+ assertEquals(SEGMENT_SIZE - 4, source.indexOf((byte) 'd'));
+ assertEquals(-1, source.indexOf((byte) 'e'));
+ }
+
+ @Test public void indexOfByteWithStartOffset() throws IOException {
+ sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c");
+ sink.emit();
+ assertEquals(-1, source.indexOf((byte) 'a', 1));
+ assertEquals(15, source.indexOf((byte) 'b', 15));
+ }
+
+ @Test public void indexOfByteWithBothOffsets() throws IOException {
+ if (factory.isOneByteAtATime()) {
+ // When run on Travis this causes out-of-memory errors.
+ return;
+ }
+ byte a = (byte) 'a';
+ byte c = (byte) 'c';
+
+ int size = SEGMENT_SIZE * 5;
+ byte[] bytes = new byte[size];
+ Arrays.fill(bytes, a);
+
+ // These are tricky places where the buffer
+ // starts, ends, or segments come together.
+ int[] points = {
+ 0, 1, 2,
+ SEGMENT_SIZE - 1, SEGMENT_SIZE, SEGMENT_SIZE + 1,
+ size / 2 - 1, size / 2, size / 2 + 1,
+ size - SEGMENT_SIZE - 1, size - SEGMENT_SIZE, size - SEGMENT_SIZE + 1,
+ size - 3, size - 2, size - 1
+ };
+
+ // In each iteration, we write c to the known point and then search for it using different
+ // windows. Some of the windows don't overlap with c's position, and therefore a match shouldn't
+ // be found.
+ for (int p : points) {
+ bytes[p] = c;
+ sink.write(bytes);
+ sink.emit();
+
+ assertEquals( p, source.indexOf(c, 0, size ));
+ assertEquals( p, source.indexOf(c, 0, p + 1 ));
+ assertEquals( p, source.indexOf(c, p, size ));
+ assertEquals( p, source.indexOf(c, p, p + 1 ));
+ assertEquals( p, source.indexOf(c, p / 2, p * 2 + 1));
+ assertEquals(-1, source.indexOf(c, 0, p / 2 ));
+ assertEquals(-1, source.indexOf(c, 0, p ));
+ assertEquals(-1, source.indexOf(c, 0, 0 ));
+ assertEquals(-1, source.indexOf(c, p, p ));
+
+ // Reset.
+ source.readUtf8();
+ bytes[p] = a;
+ }
+ }
+
+ @Test public void indexOfByteInvalidBoundsThrows() throws IOException {
+ sink.writeUtf8("abc");
+ sink.emit();
+
+ try {
+ source.indexOf((byte) 'a', -1);
+ fail("Expected failure: fromIndex < 0");
+ } catch (IllegalArgumentException expected) {
+ }
+
+ try {
+ source.indexOf((byte) 'a', 10, 0);
+ fail("Expected failure: fromIndex > toIndex");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void indexOfByteString() throws IOException {
+ assertEquals(-1, source.indexOf(ByteString.encodeUtf8("flop")));
+
+ sink.writeUtf8("flip flop");
+ sink.emit();
+ assertEquals(5, source.indexOf(ByteString.encodeUtf8("flop")));
+ source.readUtf8(); // Clear stream.
+
+ // Make sure we backtrack and resume searching after partial match.
+ sink.writeUtf8("hi hi hi hey");
+ sink.emit();
+ assertEquals(3, source.indexOf(ByteString.encodeUtf8("hi hi hey")));
+ }
+
+ @Test public void indexOfByteStringAtSegmentBoundary() throws IOException {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 1));
+ sink.writeUtf8("bcd");
+ sink.emit();
+ assertEquals(SEGMENT_SIZE - 3, source.indexOf(ByteString.encodeUtf8("aabc"), SEGMENT_SIZE - 4));
+ assertEquals(SEGMENT_SIZE - 3, source.indexOf(ByteString.encodeUtf8("aabc"), SEGMENT_SIZE - 3));
+ assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("abcd"), SEGMENT_SIZE - 2));
+ assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("abc"), SEGMENT_SIZE - 2));
+ assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("abc"), SEGMENT_SIZE - 2));
+ assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("ab"), SEGMENT_SIZE - 2));
+ assertEquals(SEGMENT_SIZE - 2, source.indexOf(ByteString.encodeUtf8("a"), SEGMENT_SIZE - 2));
+ assertEquals(SEGMENT_SIZE - 1, source.indexOf(ByteString.encodeUtf8("bc"), SEGMENT_SIZE - 2));
+ assertEquals(SEGMENT_SIZE - 1, source.indexOf(ByteString.encodeUtf8("b"), SEGMENT_SIZE - 2));
+ assertEquals(SEGMENT_SIZE, source.indexOf(ByteString.encodeUtf8("c"), SEGMENT_SIZE - 2));
+ assertEquals(SEGMENT_SIZE, source.indexOf(ByteString.encodeUtf8("c"), SEGMENT_SIZE ));
+ assertEquals(SEGMENT_SIZE + 1, source.indexOf(ByteString.encodeUtf8("d"), SEGMENT_SIZE - 2));
+ assertEquals(SEGMENT_SIZE + 1, source.indexOf(ByteString.encodeUtf8("d"), SEGMENT_SIZE + 1));
+ }
+
+ @Test public void indexOfDoesNotWrapAround() throws IOException {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 1));
+ sink.writeUtf8("bcd");
+ sink.emit();
+ assertEquals(-1, source.indexOf(ByteString.encodeUtf8("abcda"), SEGMENT_SIZE - 3));
+ }
+
+ @Test public void indexOfByteStringWithOffset() throws IOException {
+ assertEquals(-1, source.indexOf(ByteString.encodeUtf8("flop"), 1));
+
+ sink.writeUtf8("flop flip flop");
+ sink.emit();
+ assertEquals(10, source.indexOf(ByteString.encodeUtf8("flop"), 1));
+ source.readUtf8(); // Clear stream
+
+ // Make sure we backtrack and resume searching after partial match.
+ sink.writeUtf8("hi hi hi hi hey");
+ sink.emit();
+ assertEquals(6, source.indexOf(ByteString.encodeUtf8("hi hi hey"), 1));
+ }
+
+ @Test public void indexOfByteStringInvalidArgumentsThrows() throws IOException {
+ try {
+ source.indexOf(ByteString.of());
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("bytes is empty", e.getMessage());
+ }
+ try {
+ source.indexOf(ByteString.encodeUtf8("hi"), -1);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("fromIndex < 0: -1", e.getMessage());
+ }
+ }
+
+ /**
+ * With {@link Factory#ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE}, this code was extremely slow.
+ * https://github.com/square/okio/issues/171
+ */
+ @Test public void indexOfByteStringAcrossSegmentBoundaries() throws IOException {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE * 2 - 3));
+ sink.writeUtf8("bcdefg");
+ sink.emit();
+ assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("ab")));
+ assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abc")));
+ assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abcd")));
+ assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abcde")));
+ assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abcdef")));
+ assertEquals(SEGMENT_SIZE * 2 - 4, source.indexOf(ByteString.encodeUtf8("abcdefg")));
+ assertEquals(SEGMENT_SIZE * 2 - 3, source.indexOf(ByteString.encodeUtf8("bcdefg")));
+ assertEquals(SEGMENT_SIZE * 2 - 2, source.indexOf(ByteString.encodeUtf8("cdefg")));
+ assertEquals(SEGMENT_SIZE * 2 - 1, source.indexOf(ByteString.encodeUtf8("defg")));
+ assertEquals(SEGMENT_SIZE * 2, source.indexOf(ByteString.encodeUtf8("efg")));
+ assertEquals(SEGMENT_SIZE * 2 + 1, source.indexOf(ByteString.encodeUtf8("fg")));
+ assertEquals(SEGMENT_SIZE * 2 + 2, source.indexOf(ByteString.encodeUtf8("g")));
+ }
+
+ @Test public void indexOfElement() throws IOException {
+ sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c");
+ sink.emit();
+ assertEquals(0, source.indexOfElement(ByteString.encodeUtf8("DEFGaHIJK")));
+ assertEquals(1, source.indexOfElement(ByteString.encodeUtf8("DEFGHIJKb")));
+ assertEquals(SEGMENT_SIZE + 1, source.indexOfElement(ByteString.encodeUtf8("cDEFGHIJK")));
+ assertEquals(1, source.indexOfElement(ByteString.encodeUtf8("DEFbGHIc")));
+ assertEquals(-1L, source.indexOfElement(ByteString.encodeUtf8("DEFGHIJK")));
+ assertEquals(-1L, source.indexOfElement(ByteString.encodeUtf8("")));
+ }
+
+ @Test public void indexOfElementWithOffset() throws IOException {
+ sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c");
+ sink.emit();
+ assertEquals(-1, source.indexOfElement(ByteString.encodeUtf8("DEFGaHIJK"), 1));
+ assertEquals(15, source.indexOfElement(ByteString.encodeUtf8("DEFGHIJKb"), 15));
+ }
+
+ @Test public void indexOfByteWithFromIndex() throws Exception {
+ sink.writeUtf8("aaa");
+ sink.emit();
+ assertEquals(0, source.indexOf((byte) 'a'));
+ assertEquals(0, source.indexOf((byte) 'a', 0));
+ assertEquals(1, source.indexOf((byte) 'a', 1));
+ assertEquals(2, source.indexOf((byte) 'a', 2));
+ }
+
+ @Test public void indexOfByteStringWithFromIndex() throws Exception {
+ sink.writeUtf8("aaa");
+ sink.emit();
+ assertEquals(0, source.indexOf(ByteString.encodeUtf8("a")));
+ assertEquals(0, source.indexOf(ByteString.encodeUtf8("a"), 0));
+ assertEquals(1, source.indexOf(ByteString.encodeUtf8("a"), 1));
+ assertEquals(2, source.indexOf(ByteString.encodeUtf8("a"), 2));
+ }
+
+ @Test public void indexOfElementWithFromIndex() throws Exception {
+ sink.writeUtf8("aaa");
+ sink.emit();
+ assertEquals(0, source.indexOfElement(ByteString.encodeUtf8("a")));
+ assertEquals(0, source.indexOfElement(ByteString.encodeUtf8("a"), 0));
+ assertEquals(1, source.indexOfElement(ByteString.encodeUtf8("a"), 1));
+ assertEquals(2, source.indexOfElement(ByteString.encodeUtf8("a"), 2));
+ }
+
+ @Test public void request() throws IOException {
+ sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c");
+ sink.emit();
+ assertTrue(source.request(SEGMENT_SIZE + 2));
+ assertFalse(source.request(SEGMENT_SIZE + 3));
+ }
+
+ @Test public void require() throws IOException {
+ sink.writeUtf8("a").writeUtf8(repeat("b", SEGMENT_SIZE)).writeUtf8("c");
+ sink.emit();
+ source.require(SEGMENT_SIZE + 2);
+ try {
+ source.require(SEGMENT_SIZE + 3);
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void inputStream() throws Exception {
+ sink.writeUtf8("abc");
+ sink.emit();
+ InputStream in = source.inputStream();
+ byte[] bytes = { 'z', 'z', 'z' };
+ int read = in.read(bytes);
+ if (factory.isOneByteAtATime()) {
+ assertEquals(1, read);
+ assertByteArrayEquals("azz", bytes);
+
+ read = in.read(bytes);
+ assertEquals(1, read);
+ assertByteArrayEquals("bzz", bytes);
+
+ read = in.read(bytes);
+ assertEquals(1, read);
+ assertByteArrayEquals("czz", bytes);
+ } else {
+ assertEquals(3, read);
+ assertByteArrayEquals("abc", bytes);
+ }
+
+ assertEquals(-1, in.read());
+ }
+
+ @Test public void inputStreamOffsetCount() throws Exception {
+ sink.writeUtf8("abcde");
+ sink.emit();
+ InputStream in = source.inputStream();
+ byte[] bytes = { 'z', 'z', 'z', 'z', 'z' };
+ int read = in.read(bytes, 1, 3);
+ if (factory.isOneByteAtATime()) {
+ assertEquals(1, read);
+ assertByteArrayEquals("zazzz", bytes);
+ } else {
+ assertEquals(3, read);
+ assertByteArrayEquals("zabcz", bytes);
+ }
+ }
+
+ @Test public void inputStreamSkip() throws Exception {
+ sink.writeUtf8("abcde");
+ sink.emit();
+ InputStream in = source.inputStream();
+ assertEquals(4, in.skip(4));
+ assertEquals('e', in.read());
+
+ sink.writeUtf8("abcde");
+ sink.emit();
+ assertEquals(5, in.skip(10)); // Try to skip too much.
+ assertEquals(0, in.skip(1)); // Try to skip when exhausted.
+ }
+
+ @Test public void inputStreamCharByChar() throws Exception {
+ sink.writeUtf8("abc");
+ sink.emit();
+ InputStream in = source.inputStream();
+ assertEquals('a', in.read());
+ assertEquals('b', in.read());
+ assertEquals('c', in.read());
+ assertEquals(-1, in.read());
+ }
+
+ @Test public void inputStreamBounds() throws IOException {
+ sink.writeUtf8(repeat("a", 100));
+ sink.emit();
+ InputStream in = source.inputStream();
+ try {
+ in.read(new byte[100], 50, 51);
+ fail();
+ } catch (ArrayIndexOutOfBoundsException expected) {
+ }
+ }
+
+ @Test public void longHexString() throws IOException {
+ assertLongHexString("8000000000000000", 0x8000000000000000L);
+ assertLongHexString("fffffffffffffffe", 0xFFFFFFFFFFFFFFFEL);
+ assertLongHexString("FFFFFFFFFFFFFFFe", 0xFFFFFFFFFFFFFFFEL);
+ assertLongHexString("ffffffffffffffff", 0xffffffffffffffffL);
+ assertLongHexString("FFFFFFFFFFFFFFFF", 0xFFFFFFFFFFFFFFFFL);
+ assertLongHexString("0000000000000000", 0x0);
+ assertLongHexString("0000000000000001", 0x1);
+ assertLongHexString("7999999999999999", 0x7999999999999999L);
+
+ assertLongHexString("FF", 0xFF);
+ assertLongHexString("0000000000000001", 0x1);
+ }
+
+ @Test public void hexStringWithManyLeadingZeros() throws IOException {
+ assertLongHexString("00000000000000001", 0x1);
+ assertLongHexString("0000000000000000ffffffffffffffff", 0xffffffffffffffffL);
+ assertLongHexString("00000000000000007fffffffffffffff", 0x7fffffffffffffffL);
+ assertLongHexString(repeat("0", SEGMENT_SIZE + 1) + "1", 0x1);
+ }
+
+ private void assertLongHexString(String s, long expected) throws IOException {
+ sink.writeUtf8(s);
+ sink.emit();
+ long actual = source.readHexadecimalUnsignedLong();
+ assertEquals(s + " --> " + expected, expected, actual);
+ }
+
+ @Test public void longHexStringAcrossSegment() throws IOException {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 8)).writeUtf8("FFFFFFFFFFFFFFFF");
+ sink.emit();
+ source.skip(SEGMENT_SIZE - 8);
+ assertEquals(-1, source.readHexadecimalUnsignedLong());
+ }
+
+ @Test public void longHexStringTooLongThrows() throws IOException {
+ try {
+ sink.writeUtf8("fffffffffffffffff");
+ sink.emit();
+ source.readHexadecimalUnsignedLong();
+ fail();
+ } catch (NumberFormatException e) {
+ assertEquals("Number too large: fffffffffffffffff", e.getMessage());
+ }
+ }
+
+ @Test public void longHexStringTooShortThrows() throws IOException {
+ try {
+ sink.writeUtf8(" ");
+ sink.emit();
+ source.readHexadecimalUnsignedLong();
+ fail();
+ } catch (NumberFormatException e) {
+ assertEquals("Expected leading [0-9a-fA-F] character but was 0x20", e.getMessage());
+ }
+ }
+
+ @Test public void longHexEmptySourceThrows() throws IOException {
+ try {
+ sink.writeUtf8("");
+ sink.emit();
+ source.readHexadecimalUnsignedLong();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void longDecimalString() throws IOException {
+ assertLongDecimalString("-9223372036854775808", -9223372036854775808L);
+ assertLongDecimalString("-1", -1L);
+ assertLongDecimalString("0", 0L);
+ assertLongDecimalString("1", 1L);
+ assertLongDecimalString("9223372036854775807", 9223372036854775807L);
+
+ assertLongDecimalString("00000001", 1L);
+ assertLongDecimalString("-000001", -1L);
+ }
+
+ private void assertLongDecimalString(String s, long expected) throws IOException {
+ sink.writeUtf8(s);
+ sink.writeUtf8("zzz");
+ sink.emit();
+ long actual = source.readDecimalLong();
+ assertEquals(s + " --> " + expected, expected, actual);
+ assertEquals("zzz", source.readUtf8());
+ }
+
+ @Test public void longDecimalStringAcrossSegment() throws IOException {
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE - 8)).writeUtf8("1234567890123456");
+ sink.writeUtf8("zzz");
+ sink.emit();
+ source.skip(SEGMENT_SIZE - 8);
+ assertEquals(1234567890123456L, source.readDecimalLong());
+ assertEquals("zzz", source.readUtf8());
+ }
+
+ @Test public void longDecimalStringTooLongThrows() throws IOException {
+ try {
+ sink.writeUtf8("12345678901234567890"); // Too many digits.
+ sink.emit();
+ source.readDecimalLong();
+ fail();
+ } catch (NumberFormatException e) {
+ assertEquals("Number too large: 12345678901234567890", e.getMessage());
+ }
+ }
+
+ @Test public void longDecimalStringTooHighThrows() throws IOException {
+ try {
+ sink.writeUtf8("9223372036854775808"); // Right size but cannot fit.
+ sink.emit();
+ source.readDecimalLong();
+ fail();
+ } catch (NumberFormatException e) {
+ assertEquals("Number too large: 9223372036854775808", e.getMessage());
+ }
+ }
+
+ @Test public void longDecimalStringTooLowThrows() throws IOException {
+ try {
+ sink.writeUtf8("-9223372036854775809"); // Right size but cannot fit.
+ sink.emit();
+ source.readDecimalLong();
+ fail();
+ } catch (NumberFormatException e) {
+ assertEquals("Number too large: -9223372036854775809", e.getMessage());
+ }
+ }
+
+ @Test public void longDecimalStringTooShortThrows() throws IOException {
+ try {
+ sink.writeUtf8(" ");
+ sink.emit();
+ source.readDecimalLong();
+ fail();
+ } catch (NumberFormatException e) {
+ assertEquals("Expected leading [0-9] or '-' character but was 0x20", e.getMessage());
+ }
+ }
+
+ @Test public void longDecimalEmptyThrows() throws IOException {
+ try {
+ sink.writeUtf8("");
+ sink.emit();
+ source.readDecimalLong();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void codePoints() throws IOException {
+ sink.write(ByteString.decodeHex("7f"));
+ sink.emit();
+ assertEquals(0x7f, source.readUtf8CodePoint());
+
+ sink.write(ByteString.decodeHex("dfbf"));
+ sink.emit();
+ assertEquals(0x07ff, source.readUtf8CodePoint());
+
+ sink.write(ByteString.decodeHex("efbfbf"));
+ sink.emit();
+ assertEquals(0xffff, source.readUtf8CodePoint());
+
+ sink.write(ByteString.decodeHex("f48fbfbf"));
+ sink.emit();
+ assertEquals(0x10ffff, source.readUtf8CodePoint());
+ }
+
+ @Test public void decimalStringWithManyLeadingZeros() throws IOException {
+ assertLongDecimalString("00000000000000001", 1);
+ assertLongDecimalString("00000000000000009223372036854775807", 9223372036854775807L);
+ assertLongDecimalString("-00000000000000009223372036854775808", -9223372036854775808L);
+ assertLongDecimalString(repeat("0", SEGMENT_SIZE + 1) + "1", 1);
+ }
+
+ @Test public void select() throws IOException {
+ Options options = Options.Companion.of(
+ ByteString.encodeUtf8("ROCK"),
+ ByteString.encodeUtf8("SCISSORS"),
+ ByteString.encodeUtf8("PAPER"));
+
+ sink.writeUtf8("PAPER,SCISSORS,ROCK");
+ sink.emit();
+ assertEquals(2, source.select(options));
+ assertEquals(',', source.readByte());
+ assertEquals(1, source.select(options));
+ assertEquals(',', source.readByte());
+ assertEquals(0, source.select(options));
+ assertTrue(source.exhausted());
+ }
+
+ /** Note that this test crashes the VM on Android. */
+ @Test public void selectSpanningMultipleSegments() throws IOException {
+ ByteString commonPrefix = TestUtil.randomBytes(SEGMENT_SIZE + 10);
+ ByteString a = new Buffer().write(commonPrefix).writeUtf8("a").readByteString();
+ ByteString bc = new Buffer().write(commonPrefix).writeUtf8("bc").readByteString();
+ ByteString bd = new Buffer().write(commonPrefix).writeUtf8("bd").readByteString();
+ Options options = Options.Companion.of(a, bc, bd);
+
+ sink.write(bd);
+ sink.write(a);
+ sink.write(bc);
+ sink.emit();
+
+ assertEquals(2, source.select(options));
+ assertEquals(0, source.select(options));
+ assertEquals(1, source.select(options));
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void selectNotFound() throws IOException {
+ Options options = Options.Companion.of(
+ ByteString.encodeUtf8("ROCK"),
+ ByteString.encodeUtf8("SCISSORS"),
+ ByteString.encodeUtf8("PAPER"));
+
+ sink.writeUtf8("SPOCK");
+ sink.emit();
+ assertEquals(-1, source.select(options));
+ assertEquals("SPOCK", source.readUtf8());
+ }
+
+ @Test public void selectValuesHaveCommonPrefix() throws IOException {
+ Options options = Options.Companion.of(
+ ByteString.encodeUtf8("abcd"),
+ ByteString.encodeUtf8("abce"),
+ ByteString.encodeUtf8("abcc"));
+
+ sink.writeUtf8("abcc").writeUtf8("abcd").writeUtf8("abce");
+ sink.emit();
+ assertEquals(2, source.select(options));
+ assertEquals(0, source.select(options));
+ assertEquals(1, source.select(options));
+ }
+
+ @Test public void selectLongerThanSource() throws IOException {
+ Options options = Options.Companion.of(
+ ByteString.encodeUtf8("abcd"),
+ ByteString.encodeUtf8("abce"),
+ ByteString.encodeUtf8("abcc"));
+ sink.writeUtf8("abc");
+ sink.emit();
+ assertEquals(-1, source.select(options));
+ assertEquals("abc", source.readUtf8());
+ }
+
+ @Test public void selectReturnsFirstByteStringThatMatches() throws IOException {
+ Options options = Options.Companion.of(
+ ByteString.encodeUtf8("abcd"),
+ ByteString.encodeUtf8("abc"),
+ ByteString.encodeUtf8("abcde"));
+ sink.writeUtf8("abcdef");
+ sink.emit();
+ assertEquals(0, source.select(options));
+ assertEquals("ef", source.readUtf8());
+ }
+
+ @Test public void selectFromEmptySource() throws IOException {
+ Options options = Options.Companion.of(
+ ByteString.encodeUtf8("abc"),
+ ByteString.encodeUtf8("def"));
+ assertEquals(-1, source.select(options));
+ }
+
+ @Test public void selectNoByteStringsFromEmptySource() throws IOException {
+ Options options = Options.of();
+ assertEquals(-1, source.select(options));
+ }
+
+ @Test public void peek() throws IOException {
+ sink.writeUtf8("abcdefghi");
+ sink.emit();
+
+ assertEquals("abc", source.readUtf8(3));
+
+ BufferedSource peek = source.peek();
+ assertEquals("def", peek.readUtf8(3));
+ assertEquals("ghi", peek.readUtf8(3));
+ assertFalse(peek.request(1));
+
+ assertEquals("def", source.readUtf8(3));
+ }
+
+ @Test public void peekMultiple() throws IOException {
+ sink.writeUtf8("abcdefghi");
+ sink.emit();
+
+ assertEquals("abc", source.readUtf8(3));
+
+ BufferedSource peek1 = source.peek();
+ BufferedSource peek2 = source.peek();
+
+ assertEquals("def", peek1.readUtf8(3));
+
+ assertEquals("def", peek2.readUtf8(3));
+ assertEquals("ghi", peek2.readUtf8(3));
+ assertFalse(peek2.request(1));
+
+ assertEquals("ghi", peek1.readUtf8(3));
+ assertFalse(peek1.request(1));
+
+ assertEquals("def", source.readUtf8(3));
+ }
+
+ @Test public void peekLarge() throws IOException {
+ sink.writeUtf8("abcdef");
+ sink.writeUtf8(repeat("g", 2 * SEGMENT_SIZE));
+ sink.writeUtf8("hij");
+ sink.emit();
+
+ assertEquals("abc", source.readUtf8(3));
+
+ BufferedSource peek = source.peek();
+ assertEquals("def", peek.readUtf8(3));
+ peek.skip(2 * SEGMENT_SIZE);
+ assertEquals("hij", peek.readUtf8(3));
+ assertFalse(peek.request(1));
+
+ assertEquals("def", source.readUtf8(3));
+ source.skip(2 * SEGMENT_SIZE);
+ assertEquals("hij", source.readUtf8(3));
+ }
+
+ @Test public void peekInvalid() throws IOException {
+ sink.writeUtf8("abcdefghi");
+ sink.emit();
+
+ assertEquals("abc", source.readUtf8(3));
+
+ BufferedSource peek = source.peek();
+ assertEquals("def", peek.readUtf8(3));
+ assertEquals("ghi", peek.readUtf8(3));
+ assertFalse(peek.request(1));
+
+ assertEquals("def", source.readUtf8(3));
+
+ try {
+ peek.readUtf8();
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Peek source is invalid because upstream source was used", e.getMessage());
+ }
+ }
+
+ @Test public void peekSegmentThenInvalid() throws IOException {
+ sink.writeUtf8("abc");
+ sink.writeUtf8(repeat("d", 2 * SEGMENT_SIZE));
+ sink.emit();
+
+ assertEquals("abc", source.readUtf8(3));
+
+ // Peek a little data and skip the rest of the upstream source
+ BufferedSource peek = source.peek();
+ assertEquals("ddd", peek.readUtf8(3));
+ source.readAll(Okio.blackhole());
+
+ // Skip the rest of the buffered data
+ peek.skip(peek.getBuffer().size());
+
+ try {
+ peek.readByte();
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Peek source is invalid because upstream source was used", e.getMessage());
+ }
+ }
+
+ @Test public void peekDoesntReadTooMuch() throws IOException {
+ // 6 bytes in source's buffer plus 3 bytes upstream.
+ sink.writeUtf8("abcdef");
+ sink.emit();
+ source.require(6L);
+ sink.writeUtf8("ghi");
+ sink.emit();
+
+ BufferedSource peek = source.peek();
+
+ // Read 3 bytes. This reads some of the buffered data.
+ assertTrue(peek.request(3));
+ if (!(source instanceof Buffer)) {
+ assertEquals(6, source.getBuffer().size());
+ assertEquals(6, peek.getBuffer().size());
+ }
+ assertEquals("abc", peek.readUtf8(3L));
+
+ // Read 3 more bytes. This exhausts the buffered data.
+ assertTrue(peek.request(3));
+ if (!(source instanceof Buffer)) {
+ assertEquals(6, source.getBuffer().size());
+ assertEquals(3, peek.getBuffer().size());
+ }
+ assertEquals("def", peek.readUtf8(3L));
+
+ // Read 3 more bytes. This draws new bytes.
+ assertTrue(peek.request(3));
+ assertEquals(9, source.getBuffer().size());
+ assertEquals(3, peek.getBuffer().size());
+ assertEquals("ghi", peek.readUtf8(3L));
+ }
+
+ @Test public void rangeEquals() throws IOException {
+ sink.writeUtf8("A man, a plan, a canal. Panama.");
+ sink.emit();
+ assertTrue(source.rangeEquals(7 , ByteString.encodeUtf8("a plan")));
+ assertTrue(source.rangeEquals(0 , ByteString.encodeUtf8("A man")));
+ assertTrue(source.rangeEquals(24, ByteString.encodeUtf8("Panama")));
+ assertFalse(source.rangeEquals(24, ByteString.encodeUtf8("Panama. Panama. Panama.")));
+ }
+
+ @Test public void rangeEqualsWithOffsetAndCount() throws IOException {
+ sink.writeUtf8("A man, a plan, a canal. Panama.");
+ sink.emit();
+ assertTrue(source.rangeEquals(7 , ByteString.encodeUtf8("aaa plannn"), 2, 6));
+ assertTrue(source.rangeEquals(0 , ByteString.encodeUtf8("AAA mannn"), 2, 5));
+ assertTrue(source.rangeEquals(24, ByteString.encodeUtf8("PPPanamaaa"), 2, 6));
+ }
+
+ @Test public void rangeEqualsOnlyReadsUntilMismatch() throws IOException {
+ assumeTrue(factory == Factory.ONE_BYTE_AT_A_TIME_BUFFERED_SOURCE); // Other sources read in chunks anyway.
+
+ sink.writeUtf8("A man, a plan, a canal. Panama.");
+ sink.emit();
+ assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A man.")));
+ assertEquals("A man,", source.getBuffer().readUtf8());
+ }
+
+ @Test public void rangeEqualsArgumentValidation() throws IOException {
+ // Negative source offset.
+ assertFalse(source.rangeEquals(-1, ByteString.encodeUtf8("A")));
+ // Negative bytes offset.
+ assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), -1, 1));
+ // Bytes offset longer than bytes length.
+ assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), 2, 1));
+ // Negative byte count.
+ assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), 0, -1));
+ // Byte count longer than bytes length.
+ assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), 0, 2));
+ // Bytes offset plus byte count longer than bytes length.
+ assertFalse(source.rangeEquals(0, ByteString.encodeUtf8("A"), 1, 1));
+ }
+
+ @Test public void readNioBuffer() throws Exception {
+ String expected = factory.isOneByteAtATime() ? "a" : "abcdefg";
+ sink.writeUtf8("abcdefg");
+ sink.emit();
+
+ ByteBuffer nioByteBuffer = ByteBuffer.allocate(1024);
+ int byteCount = source.read(nioByteBuffer);
+ assertEquals(expected.length(), byteCount);
+ assertEquals(expected.length(), nioByteBuffer.position());
+ assertEquals(nioByteBuffer.capacity(), nioByteBuffer.limit());
+
+ nioByteBuffer.flip();
+ byte[] data = new byte[expected.length()];
+ nioByteBuffer.get(data);
+ assertEquals(expected, new String(data));
+ }
+
+ /** Note that this test crashes the VM on Android. */
+ @Test public void readLargeNioBufferOnlyReadsOneSegment() throws Exception {
+ String expected = factory.isOneByteAtATime()
+ ? "a"
+ : repeat("a", SEGMENT_SIZE);
+ sink.writeUtf8(repeat("a", SEGMENT_SIZE * 4));
+ sink.emit();
+
+ ByteBuffer nioByteBuffer = ByteBuffer.allocate(SEGMENT_SIZE * 3);
+ int byteCount = source.read(nioByteBuffer);
+ assertEquals(expected.length(), byteCount);
+ assertEquals(expected.length(), nioByteBuffer.position());
+ assertEquals(nioByteBuffer.capacity(), nioByteBuffer.limit());
+
+ nioByteBuffer.flip();
+ byte[] data = new byte[expected.length()];
+ nioByteBuffer.get(data);
+ assertEquals(expected, new String(data));
+ }
+
+ @Test public void factorySegmentSizes() throws Exception {
+ sink.writeUtf8("abc");
+ sink.emit();
+ source.require(3);
+ if (factory.isOneByteAtATime()) {
+ assertEquals(Arrays.asList(1, 1, 1), TestUtil.segmentSizes(source.getBuffer()));
+ } else {
+ assertEquals(Collections.singletonList(3), TestUtil.segmentSizes(source.getBuffer()));
+ }
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/ByteStringJavaTest.java b/okio/src/jvmTest/java/okio/ByteStringJavaTest.java
new file mode 100644
index 00000000..f1b6624e
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/ByteStringJavaTest.java
@@ -0,0 +1,633 @@
+/*
+ * Copyright 2014 Square Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import kotlin.text.Charsets;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import static okio.TestUtil.assertByteArraysEquals;
+import static okio.TestUtil.assertEquivalent;
+import static okio.TestUtil.makeSegments;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(Parameterized.class)
+public final class ByteStringJavaTest {
+ interface Factory {
+ Factory BYTE_STRING = new Factory() {
+ @Override public ByteString decodeHex(String hex) {
+ return ByteString.decodeHex(hex);
+ }
+
+ @Override public ByteString encodeUtf8(String s) {
+ return ByteString.encodeUtf8(s);
+ }
+ };
+
+ Factory SEGMENTED_BYTE_STRING = new Factory() {
+ @Override public ByteString decodeHex(String hex) {
+ Buffer buffer = new Buffer();
+ buffer.write(ByteString.decodeHex(hex));
+ return buffer.snapshot();
+ }
+
+ @Override public ByteString encodeUtf8(String s) {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8(s);
+ return buffer.snapshot();
+ }
+ };
+
+ Factory ONE_BYTE_PER_SEGMENT = new Factory() {
+ @Override public ByteString decodeHex(String hex) {
+ return makeSegments(ByteString.decodeHex(hex));
+ }
+
+ @Override public ByteString encodeUtf8(String s) {
+ return makeSegments(ByteString.encodeUtf8(s));
+ }
+ };
+
+ ByteString decodeHex(String hex);
+ ByteString encodeUtf8(String s);
+ }
+
+ @Parameters(name = "{1}")
+ public static List<Object[]> parameters() {
+ return Arrays.asList(
+ new Object[] { Factory.BYTE_STRING, "ByteString" },
+ new Object[] { Factory.SEGMENTED_BYTE_STRING, "SegmentedByteString" },
+ new Object[] { Factory.ONE_BYTE_PER_SEGMENT, "SegmentedByteString (one-at-a-time)" });
+ }
+
+ @Parameter(0) public Factory factory;
+ @Parameter(1) public String name;
+
+ @Test public void ofCopy() {
+ byte[] bytes = "Hello, World!".getBytes(Charsets.UTF_8);
+ ByteString byteString = ByteString.of(bytes);
+ // Verify that the bytes were copied out.
+ bytes[4] = (byte) 'a';
+ assertEquals("Hello, World!", byteString.utf8());
+ }
+
+ @Test public void ofCopyRange() {
+ byte[] bytes = "Hello, World!".getBytes(Charsets.UTF_8);
+ ByteString byteString = ByteString.of(bytes, 2, 9);
+ // Verify that the bytes were copied out.
+ bytes[4] = (byte) 'a';
+ assertEquals("llo, Worl", byteString.utf8());
+ }
+
+ @Test public void ofByteBuffer() {
+ byte[] bytes = "Hello, World!".getBytes(Charsets.UTF_8);
+ ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+ byteBuffer.position(2).limit(11);
+ ByteString byteString = ByteString.of(byteBuffer);
+ // Verify that the bytes were copied out.
+ byteBuffer.put(4, (byte) 'a');
+ assertEquals("llo, Worl", byteString.utf8());
+ }
+
+ @Test public void getByte() throws Exception {
+ ByteString byteString = factory.decodeHex("ab12");
+ assertEquals(-85, byteString.getByte(0));
+ assertEquals(18, byteString.getByte(1));
+ }
+
+ @Test public void getByteOutOfBounds() throws Exception {
+ ByteString byteString = factory.decodeHex("ab12");
+ try {
+ byteString.getByte(2);
+ fail();
+ } catch (IndexOutOfBoundsException expected) {
+ }
+ }
+
+ @Test public void startsWithByteString() throws Exception {
+ ByteString byteString = factory.decodeHex("112233");
+ assertTrue(byteString.startsWith(ByteString.decodeHex("")));
+ assertTrue(byteString.startsWith(ByteString.decodeHex("11")));
+ assertTrue(byteString.startsWith(ByteString.decodeHex("1122")));
+ assertTrue(byteString.startsWith(ByteString.decodeHex("112233")));
+ assertFalse(byteString.startsWith(ByteString.decodeHex("2233")));
+ assertFalse(byteString.startsWith(ByteString.decodeHex("11223344")));
+ assertFalse(byteString.startsWith(ByteString.decodeHex("112244")));
+ }
+
+ @Test public void endsWithByteString() throws Exception {
+ ByteString byteString = factory.decodeHex("112233");
+ assertTrue(byteString.endsWith(ByteString.decodeHex("")));
+ assertTrue(byteString.endsWith(ByteString.decodeHex("33")));
+ assertTrue(byteString.endsWith(ByteString.decodeHex("2233")));
+ assertTrue(byteString.endsWith(ByteString.decodeHex("112233")));
+ assertFalse(byteString.endsWith(ByteString.decodeHex("1122")));
+ assertFalse(byteString.endsWith(ByteString.decodeHex("00112233")));
+ assertFalse(byteString.endsWith(ByteString.decodeHex("002233")));
+ }
+
+ @Test public void startsWithByteArray() throws Exception {
+ ByteString byteString = factory.decodeHex("112233");
+ assertTrue(byteString.startsWith(ByteString.decodeHex("").toByteArray()));
+ assertTrue(byteString.startsWith(ByteString.decodeHex("11").toByteArray()));
+ assertTrue(byteString.startsWith(ByteString.decodeHex("1122").toByteArray()));
+ assertTrue(byteString.startsWith(ByteString.decodeHex("112233").toByteArray()));
+ assertFalse(byteString.startsWith(ByteString.decodeHex("2233").toByteArray()));
+ assertFalse(byteString.startsWith(ByteString.decodeHex("11223344").toByteArray()));
+ assertFalse(byteString.startsWith(ByteString.decodeHex("112244").toByteArray()));
+ }
+
+ @Test public void endsWithByteArray() throws Exception {
+ ByteString byteString = factory.decodeHex("112233");
+ assertTrue(byteString.endsWith(ByteString.decodeHex("").toByteArray()));
+ assertTrue(byteString.endsWith(ByteString.decodeHex("33").toByteArray()));
+ assertTrue(byteString.endsWith(ByteString.decodeHex("2233").toByteArray()));
+ assertTrue(byteString.endsWith(ByteString.decodeHex("112233").toByteArray()));
+ assertFalse(byteString.endsWith(ByteString.decodeHex("1122").toByteArray()));
+ assertFalse(byteString.endsWith(ByteString.decodeHex("00112233").toByteArray()));
+ assertFalse(byteString.endsWith(ByteString.decodeHex("002233").toByteArray()));
+ }
+
+ @Test public void indexOfByteString() throws Exception {
+ ByteString byteString = factory.decodeHex("112233");
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233")));
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex("1122")));
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex("11")));
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex("11"), 0));
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex("")));
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex(""), 0));
+ assertEquals(1, byteString.indexOf(ByteString.decodeHex("2233")));
+ assertEquals(1, byteString.indexOf(ByteString.decodeHex("22")));
+ assertEquals(1, byteString.indexOf(ByteString.decodeHex("22"), 1));
+ assertEquals(1, byteString.indexOf(ByteString.decodeHex(""), 1));
+ assertEquals(2, byteString.indexOf(ByteString.decodeHex("33")));
+ assertEquals(2, byteString.indexOf(ByteString.decodeHex("33"), 2));
+ assertEquals(2, byteString.indexOf(ByteString.decodeHex(""), 2));
+ assertEquals(3, byteString.indexOf(ByteString.decodeHex(""), 3));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112233"), 1));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex("44")));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex("11223344")));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112244")));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112233"), 1));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex("2233"), 2));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex("33"), 3));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex(""), 4));
+ }
+
+ @Test public void indexOfWithOffset() throws Exception {
+ ByteString byteString = factory.decodeHex("112233112233");
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233"), -1));
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233"), 0));
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233")));
+ assertEquals(3, byteString.indexOf(ByteString.decodeHex("112233"), 1));
+ assertEquals(3, byteString.indexOf(ByteString.decodeHex("112233"), 2));
+ assertEquals(3, byteString.indexOf(ByteString.decodeHex("112233"), 3));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112233"), 4));
+ }
+
+ @Test public void indexOfByteArray() throws Exception {
+ ByteString byteString = factory.decodeHex("112233");
+ assertEquals(0, byteString.indexOf(ByteString.decodeHex("112233").toByteArray()));
+ assertEquals(1, byteString.indexOf(ByteString.decodeHex("2233").toByteArray()));
+ assertEquals(2, byteString.indexOf(ByteString.decodeHex("33").toByteArray()));
+ assertEquals(-1, byteString.indexOf(ByteString.decodeHex("112244").toByteArray()));
+ }
+
+ @Test public void lastIndexOfByteString() throws Exception {
+ ByteString byteString = factory.decodeHex("112233");
+ assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("112233")));
+ assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("1122")));
+ assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("11")));
+ assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("11"), 3));
+ assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("11"), 0));
+ assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex(""), 0));
+ assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("2233")));
+ assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("22")));
+ assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("22"), 3));
+ assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("22"), 1));
+ assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex(""), 1));
+ assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex("33")));
+ assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex("33"), 3));
+ assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex("33"), 2));
+ assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex(""), 2));
+ assertEquals(3, byteString.lastIndexOf(ByteString.decodeHex(""), 3));
+ assertEquals(3, byteString.lastIndexOf(ByteString.decodeHex("")));
+ assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("112233"), -1));
+ assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("112233"), -2));
+ assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("44")));
+ assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("11223344")));
+ assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("112244")));
+ assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("2233"), 0));
+ assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex("33"), 1));
+ assertEquals(-1, byteString.lastIndexOf(ByteString.decodeHex(""), -1));
+ }
+
+ @Test public void lastIndexOfByteArray() throws Exception {
+ ByteString byteString = factory.decodeHex("112233");
+ assertEquals(0, byteString.lastIndexOf(ByteString.decodeHex("112233").toByteArray()));
+ assertEquals(1, byteString.lastIndexOf(ByteString.decodeHex("2233").toByteArray()));
+ assertEquals(2, byteString.lastIndexOf(ByteString.decodeHex("33").toByteArray()));
+ assertEquals(3, byteString.lastIndexOf(ByteString.decodeHex("").toByteArray()));
+ }
+
+ @SuppressWarnings("SelfEquals")
+ @Test public void equals() throws Exception {
+ ByteString byteString = factory.decodeHex("000102");
+ assertTrue(byteString.equals(byteString));
+ assertTrue(byteString.equals(ByteString.decodeHex("000102")));
+ assertTrue(factory.decodeHex("").equals(ByteString.EMPTY));
+ assertTrue(factory.decodeHex("").equals(ByteString.of()));
+ assertTrue(ByteString.EMPTY.equals(factory.decodeHex("")));
+ assertTrue(ByteString.of().equals(factory.decodeHex("")));
+ assertFalse(byteString.equals(new Object()));
+ assertFalse(byteString.equals(ByteString.decodeHex("000201")));
+ }
+
+ private final String bronzeHorseman = "На берегу пустынных волн";
+
+ @Test public void utf8() throws Exception {
+ ByteString byteString = factory.encodeUtf8(bronzeHorseman);
+ assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(Charsets.UTF_8));
+ assertTrue(byteString.equals(ByteString.of(bronzeHorseman.getBytes(Charsets.UTF_8))));
+ assertEquals(byteString.utf8(), bronzeHorseman);
+ }
+
+ @Test public void encodeNullCharset() throws Exception {
+ try {
+ ByteString.encodeString("hello", null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test public void encodeNullString() throws Exception {
+ try {
+ ByteString.encodeString(null, Charset.forName("UTF-8"));
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test public void decodeNullCharset() throws Exception {
+ try {
+ ByteString.of().string(null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test public void encodeDecodeStringUtf8() throws Exception {
+ Charset utf8 = Charset.forName("UTF-8");
+ ByteString byteString = ByteString.encodeString(bronzeHorseman, utf8);
+ assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(utf8));
+ assertEquals(byteString, ByteString.decodeHex("d09dd0b020d0b1d0b5d180d0b5d0b3d18320d0bfd183d181"
+ + "d182d18bd0bdd0bdd18bd18520d0b2d0bed0bbd0bd"));
+ assertEquals(bronzeHorseman, byteString.string(utf8));
+ }
+
+ @Test public void encodeDecodeStringUtf16be() throws Exception {
+ Charset utf16be = Charset.forName("UTF-16BE");
+ ByteString byteString = ByteString.encodeString(bronzeHorseman, utf16be);
+ assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(utf16be));
+ assertEquals(byteString, ByteString.decodeHex("041d043000200431043504400435043304430020043f0443"
+ + "04410442044b043d043d044b044500200432043e043b043d"));
+ assertEquals(bronzeHorseman, byteString.string(utf16be));
+ }
+
+ @Test public void encodeDecodeStringUtf32be() throws Exception {
+ Charset utf32be = Charset.forName("UTF-32BE");
+ ByteString byteString = ByteString.encodeString(bronzeHorseman, utf32be);
+ assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(utf32be));
+ assertEquals(byteString, ByteString.decodeHex("0000041d0000043000000020000004310000043500000440"
+ + "000004350000043300000443000000200000043f0000044300000441000004420000044b0000043d0000043d"
+ + "0000044b0000044500000020000004320000043e0000043b0000043d"));
+ assertEquals(bronzeHorseman, byteString.string(utf32be));
+ }
+
+ @Test public void encodeDecodeStringAsciiIsLossy() throws Exception {
+ Charset ascii = Charset.forName("US-ASCII");
+ ByteString byteString = ByteString.encodeString(bronzeHorseman, ascii);
+ assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes(ascii));
+ assertEquals(byteString,
+ ByteString.decodeHex("3f3f203f3f3f3f3f3f203f3f3f3f3f3f3f3f3f203f3f3f3f"));
+ assertEquals("?? ?????? ????????? ????", byteString.string(ascii));
+ }
+
+ @Test public void decodeMalformedStringReturnsReplacementCharacter() throws Exception {
+ Charset utf16be = Charset.forName("UTF-16BE");
+ String string = ByteString.decodeHex("04").string(utf16be);
+ assertEquals("\ufffd", string);
+ }
+
+ @Test public void testHashCode() throws Exception {
+ ByteString byteString = factory.decodeHex("0102");
+ assertEquals(byteString.hashCode(), byteString.hashCode());
+ assertEquals(byteString.hashCode(), ByteString.decodeHex("0102").hashCode());
+ }
+
+ @Test public void read() throws Exception {
+ InputStream in = new ByteArrayInputStream("abc".getBytes(Charsets.UTF_8));
+ assertEquals(ByteString.decodeHex("6162"), ByteString.read(in, 2));
+ assertEquals(ByteString.decodeHex("63"), ByteString.read(in, 1));
+ assertEquals(ByteString.of(), ByteString.read(in, 0));
+ }
+
+ @Test public void readAndToLowercase() throws Exception {
+ InputStream in = new ByteArrayInputStream("ABC".getBytes(Charsets.UTF_8));
+ assertEquals(ByteString.encodeUtf8("ab"), ByteString.read(in, 2).toAsciiLowercase());
+ assertEquals(ByteString.encodeUtf8("c"), ByteString.read(in, 1).toAsciiLowercase());
+ assertEquals(ByteString.EMPTY, ByteString.read(in, 0).toAsciiLowercase());
+ }
+
+ @Test public void toAsciiLowerCaseNoUppercase() throws Exception {
+ ByteString s = factory.encodeUtf8("a1_+");
+ assertEquals(s, s.toAsciiLowercase());
+ if (factory == Factory.BYTE_STRING) {
+ assertSame(s, s.toAsciiLowercase());
+ }
+ }
+
+ @Test public void toAsciiAllUppercase() throws Exception {
+ assertEquals(ByteString.encodeUtf8("ab"), factory.encodeUtf8("AB").toAsciiLowercase());
+ }
+
+ @Test public void toAsciiStartsLowercaseEndsUppercase() throws Exception {
+ assertEquals(ByteString.encodeUtf8("abcd"), factory.encodeUtf8("abCD").toAsciiLowercase());
+ }
+
+ @Test public void readAndToUppercase() throws Exception {
+ InputStream in = new ByteArrayInputStream("abc".getBytes(Charsets.UTF_8));
+ assertEquals(ByteString.encodeUtf8("AB"), ByteString.read(in, 2).toAsciiUppercase());
+ assertEquals(ByteString.encodeUtf8("C"), ByteString.read(in, 1).toAsciiUppercase());
+ assertEquals(ByteString.EMPTY, ByteString.read(in, 0).toAsciiUppercase());
+ }
+
+ @Test public void toAsciiStartsUppercaseEndsLowercase() throws Exception {
+ assertEquals(ByteString.encodeUtf8("ABCD"), factory.encodeUtf8("ABcd").toAsciiUppercase());
+ }
+
+ @Test public void substring() throws Exception {
+ ByteString byteString = factory.encodeUtf8("Hello, World!");
+
+ assertEquals(byteString.substring(0), byteString);
+ assertEquals(byteString.substring(0, 5), ByteString.encodeUtf8("Hello"));
+ assertEquals(byteString.substring(7), ByteString.encodeUtf8("World!"));
+ assertEquals(byteString.substring(6, 6), ByteString.encodeUtf8(""));
+ }
+
+ @Test public void substringWithInvalidBounds() throws Exception {
+ ByteString byteString = factory.encodeUtf8("Hello, World!");
+
+ try {
+ byteString.substring(-1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+
+ try {
+ byteString.substring(0, 14);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+
+ try {
+ byteString.substring(8, 7);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void write() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ factory.decodeHex("616263").write(out);
+ assertByteArraysEquals(new byte[] { 0x61, 0x62, 0x63 }, out.toByteArray());
+ }
+
+ @Test public void encodeBase64() {
+ assertEquals("", factory.encodeUtf8("").base64());
+ assertEquals("AA==", factory.encodeUtf8("\u0000").base64());
+ assertEquals("AAA=", factory.encodeUtf8("\u0000\u0000").base64());
+ assertEquals("AAAA", factory.encodeUtf8("\u0000\u0000\u0000").base64());
+ assertEquals("SG93IG1hbnkgbGluZXMgb2YgY29kZSBhcmUgdGhlcmU/ICdib3V0IDIgbWlsbGlvbi4=",
+ factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64());
+ }
+
+ @Test public void encodeBase64Url() {
+ assertEquals("", factory.encodeUtf8("").base64Url());
+ assertEquals("AA==", factory.encodeUtf8("\u0000").base64Url());
+ assertEquals("AAA=", factory.encodeUtf8("\u0000\u0000").base64Url());
+ assertEquals("AAAA", factory.encodeUtf8("\u0000\u0000\u0000").base64Url());
+ assertEquals("SG93IG1hbnkgbGluZXMgb2YgY29kZSBhcmUgdGhlcmU_ICdib3V0IDIgbWlsbGlvbi4=",
+ factory.encodeUtf8("How many lines of code are there? 'bout 2 million.").base64Url());
+ }
+
+ @Test public void ignoreUnnecessaryPadding() {
+ assertEquals("", ByteString.decodeBase64("====").utf8());
+ assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64("AAAA====").utf8());
+ }
+
+ @Test public void decodeBase64() {
+ assertEquals("", ByteString.decodeBase64("").utf8());
+ assertEquals(null, ByteString.decodeBase64("/===")); // Can't do anything with 6 bits!
+ assertEquals(ByteString.decodeHex("ff"), ByteString.decodeBase64("//=="));
+ assertEquals(ByteString.decodeHex("ff"), ByteString.decodeBase64("__=="));
+ assertEquals(ByteString.decodeHex("ffff"), ByteString.decodeBase64("///="));
+ assertEquals(ByteString.decodeHex("ffff"), ByteString.decodeBase64("___="));
+ assertEquals(ByteString.decodeHex("ffffff"), ByteString.decodeBase64("////"));
+ assertEquals(ByteString.decodeHex("ffffff"), ByteString.decodeBase64("____"));
+ assertEquals(ByteString.decodeHex("ffffffffffff"), ByteString.decodeBase64("////////"));
+ assertEquals(ByteString.decodeHex("ffffffffffff"), ByteString.decodeBase64("________"));
+ assertEquals("What's to be scared about? It's just a little hiccup in the power...",
+ ByteString.decodeBase64("V2hhdCdzIHRvIGJlIHNjYXJlZCBhYm91dD8gSXQncyBqdXN0IGEgbGl0dGxlIGhpY2"
+ + "N1cCBpbiB0aGUgcG93ZXIuLi4=").utf8());
+ // Uses two encoding styles. Malformed, but supported as a side-effect.
+ assertEquals(ByteString.decodeHex("ffffff"), ByteString.decodeBase64("__//"));
+ }
+
+ @Test public void decodeBase64WithWhitespace() {
+ assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA AA ").utf8());
+ assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA A\r\nA ").utf8());
+ assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64("AA AA").utf8());
+ assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA AA ").utf8());
+ assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64(" AA A\r\nA ").utf8());
+ assertEquals("\u0000\u0000\u0000", ByteString.decodeBase64("A AAA").utf8());
+ assertEquals("", ByteString.decodeBase64(" ").utf8());
+ }
+
+ @Test public void encodeHex() throws Exception {
+ assertEquals("000102", ByteString.of((byte) 0x0, (byte) 0x1, (byte) 0x2).hex());
+ }
+
+ @Test public void decodeHex() throws Exception {
+ assertEquals(ByteString.of((byte) 0x0, (byte) 0x1, (byte) 0x2), ByteString.decodeHex("000102"));
+ }
+
+ @Test public void decodeHexOddNumberOfChars() throws Exception {
+ try {
+ ByteString.decodeHex("aaa");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void decodeHexInvalidChar() throws Exception {
+ try {
+ ByteString.decodeHex("a\u0000");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void toStringOnEmpty() {
+ assertEquals("[size=0]", factory.decodeHex("").toString());
+ }
+
+ @Test public void toStringOnShortText() {
+ assertEquals("[text=Tyrannosaur]",
+ factory.encodeUtf8("Tyrannosaur").toString());
+ assertEquals("[text=təˈranəˌsôr]",
+ factory.decodeHex("74c999cb8872616ec999cb8c73c3b472").toString());
+ }
+
+ @Test public void toStringOnLongTextIsTruncated() {
+ String raw = "Um, I'll tell you the problem with the scientific power that you're using here, "
+ + "it didn't require any discipline to attain it. You read what others had done and you "
+ + "took the next step. You didn't earn the knowledge for yourselves, so you don't take any "
+ + "responsibility for it. You stood on the shoulders of geniuses to accomplish something "
+ + "as fast as you could, and before you even knew what you had, you patented it, and "
+ + "packaged it, and slapped it on a plastic lunchbox, and now you're selling it, you wanna "
+ + "sell it.";
+ assertEquals("[size=517 text=Um, I'll tell you the problem with the scientific power that "
+ + "you…]", factory.encodeUtf8(raw).toString());
+ String war = "Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 𝛄𝓸𝘂'𝒓𝗲 υ𝖘𝓲𝗇ɡ 𝕙𝚎𝑟e, "
+ + "𝛊𝓽 ⅆ𝕚𝐝𝝿'𝗍 𝔯𝙚𝙦ᴜ𝜾𝒓𝘦 𝔞𝘯𝐲 ԁ𝜄𝑠𝚌ι𝘱lι𝒏e 𝑡𝜎 𝕒𝚝𝖙𝓪і𝞹 𝔦𝚝. 𝒀ο𝗎 𝔯𝑒⍺𝖉 w𝐡𝝰𝔱 𝞂𝞽һ𝓮𝓇ƽ հ𝖺𝖉 ⅾ𝛐𝝅ⅇ 𝝰πԁ 𝔂ᴑᴜ 𝓉ﮨ၀𝚔 "
+ + "т𝒽𝑒 𝗇𝕖ⅹ𝚝 𝔰𝒕е𝓅. 𝘠ⲟ𝖚 𝖉ⅰԁ𝝕'τ 𝙚𝚊r𝞹 𝘵Ꮒ𝖾 𝝒𝐧هwl𝑒𝖉ƍ𝙚 𝓯૦r 𝔂𝞼𝒖𝕣𝑠𝕖l𝙫𝖊𝓼, 𐑈о y𝘰𝒖 ⅆە𝗇't 𝜏α𝒌𝕖 𝛂𝟉ℽ "
+ + "𝐫ⅇ𝗌ⲣ๐ϖ𝖘ꙇᖯ𝓲l𝓲𝒕𝘆 𝐟𝞼𝘳 𝚤𝑡. 𝛶𝛔𝔲 s𝕥σσ𝐝 ﮩ𝕟 𝒕𝗁𝔢 𝘴𝐡𝜎ᴜlⅾ𝓮𝔯𝚜 𝛐𝙛 ᶃ𝚎ᴨᎥս𝚜𝘦𝓈 𝓽𝞸 a𝒄𝚌𝞸mρl𝛊ꜱ𝐡 𝓈𝚘m𝚎𝞃𝔥⍳𝞹𝔤 𝐚𝗌 𝖋a𝐬𝒕 "
+ + "αs γ𝛐𝕦 𝔠ﻫ𝛖lԁ, 𝚊π𝑑 Ь𝑒𝙛૦𝓇𝘦 𝓎٥𝖚 ⅇvℯ𝝅 𝜅ո𝒆w w𝗵𝒂𝘁 ᶌ੦𝗎 h𝐚𝗱, 𝜸ﮨ𝒖 𝓹𝝰𝔱𝖾𝗇𝓽𝔢ⅆ і𝕥, 𝚊𝜛𝓭 𝓹𝖺ⅽϰ𝘢ℊеᏧ 𝑖𝞃, "
+ + "𝐚𝛑ꓒ 𝙨l𝔞р𝘱𝔢𝓭 ɩ𝗍 ہ𝛑 𝕒 pl𝛂ѕᴛ𝗂𝐜 l𝞄ℼ𝔠𝒽𝑏ﮪ⨯, 𝔞ϖ𝒹 n𝛔w 𝛾𝐨𝞄'𝗿𝔢 ꜱ℮ll𝙞nɡ ɩ𝘁, 𝙮𝕠𝛖 w𝑎ℼ𝚗𝛂 𝕤𝓮ll 𝙞𝓉.";
+ assertEquals( "[size=1496 text=Սm, I'll 𝓽𝖾ll ᶌօ𝘂 ᴛℎ℮ 𝜚𝕣०bl𝖾m wі𝕥𝒽 𝘵𝘩𝐞 𝓼𝙘𝐢𝔢𝓷𝗍𝜄𝚏𝑖c 𝛠𝝾w𝚎𝑟 𝕥h⍺𝞃 "
+ + "𝛄𝓸𝘂…]", factory.encodeUtf8(war).toString());
+ }
+
+ @Test public void toStringOnTextWithNewlines() {
+ // Instead of emitting a literal newline in the toString(), these are escaped as "\n".
+ assertEquals("[text=a\\r\\nb\\nc\\rd\\\\e]",
+ factory.encodeUtf8("a\r\nb\nc\rd\\e").toString());
+ }
+
+ @Test public void toStringOnData() {
+ ByteString byteString = factory.decodeHex(""
+ + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55"
+ + "4bf0b54023c29b624de9ef9c2f931efc580f9afb");
+ assertEquals("[hex="
+ + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55"
+ + "4bf0b54023c29b624de9ef9c2f931efc580f9afb]", byteString.toString());
+ }
+
+ @Test public void toStringOnLongDataIsTruncated() {
+ ByteString byteString = factory.decodeHex(""
+ + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55"
+ + "4bf0b54023c29b624de9ef9c2f931efc580f9afba1");
+ assertEquals("[size=65 hex="
+ + "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da712c82bcd4d55"
+ + "4bf0b54023c29b624de9ef9c2f931efc580f9afb…]", byteString.toString());
+ }
+
+ @Test public void javaSerializationTestNonEmpty() throws Exception {
+ ByteString byteString = factory.encodeUtf8(bronzeHorseman);
+ assertEquivalent(byteString, TestUtil.<ByteString>reserialize(byteString));
+ }
+
+ @Test public void javaSerializationTestEmpty() throws Exception {
+ ByteString byteString = factory.decodeHex("");
+ assertEquivalent(byteString, TestUtil.<ByteString>reserialize(byteString));
+ }
+
+ @Test public void compareToSingleBytes() throws Exception {
+ List<ByteString> originalByteStrings = Arrays.asList(
+ factory.decodeHex("00"),
+ factory.decodeHex("01"),
+ factory.decodeHex("7e"),
+ factory.decodeHex("7f"),
+ factory.decodeHex("80"),
+ factory.decodeHex("81"),
+ factory.decodeHex("fe"),
+ factory.decodeHex("ff"));
+
+ List<ByteString> sortedByteStrings = new ArrayList<>(originalByteStrings);
+ Collections.shuffle(sortedByteStrings, new Random(0));
+ Collections.sort(sortedByteStrings);
+
+ assertEquals(originalByteStrings, sortedByteStrings);
+ }
+
+ @Test public void compareToMultipleBytes() throws Exception {
+ List<ByteString> originalByteStrings = Arrays.asList(
+ factory.decodeHex(""),
+ factory.decodeHex("00"),
+ factory.decodeHex("0000"),
+ factory.decodeHex("000000"),
+ factory.decodeHex("00000000"),
+ factory.decodeHex("0000000000"),
+ factory.decodeHex("0000000001"),
+ factory.decodeHex("000001"),
+ factory.decodeHex("00007f"),
+ factory.decodeHex("0000ff"),
+ factory.decodeHex("000100"),
+ factory.decodeHex("000101"),
+ factory.decodeHex("007f00"),
+ factory.decodeHex("00ff00"),
+ factory.decodeHex("010000"),
+ factory.decodeHex("010001"),
+ factory.decodeHex("01007f"),
+ factory.decodeHex("0100ff"),
+ factory.decodeHex("010100"),
+ factory.decodeHex("01010000"),
+ factory.decodeHex("0101000000"),
+ factory.decodeHex("0101000001"),
+ factory.decodeHex("010101"),
+ factory.decodeHex("7f0000"),
+ factory.decodeHex("7f0000ffff"),
+ factory.decodeHex("ffffff"));
+
+ List<ByteString> sortedByteStrings = new ArrayList<>(originalByteStrings);
+ Collections.shuffle(sortedByteStrings, new Random(0));
+ Collections.sort(sortedByteStrings);
+
+ assertEquals(originalByteStrings, sortedByteStrings);
+ }
+
+ @Test public void asByteBuffer() {
+ assertEquals(0x42, ByteString.of((byte) 0x41, (byte) 0x42, (byte) 0x43).asByteBuffer().get(1));
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/DeflaterSinkTest.java b/okio/src/jvmTest/java/okio/DeflaterSinkTest.java
new file mode 100644
index 00000000..f0a31f00
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/DeflaterSinkTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.Deflater;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import org.junit.Test;
+
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static okio.TestUtil.randomBytes;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class DeflaterSinkTest {
+ @Test public void deflateWithClose() throws Exception {
+ Buffer data = new Buffer();
+ String original = "They're moving in herds. They do move in herds.";
+ data.writeUtf8(original);
+ Buffer sink = new Buffer();
+ DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
+ deflaterSink.write(data, data.size());
+ deflaterSink.close();
+ Buffer inflated = inflate(sink);
+ assertEquals(original, inflated.readUtf8());
+ }
+
+ @Test public void deflateWithSyncFlush() throws Exception {
+ String original = "Yes, yes, yes. That's why we're taking extreme precautions.";
+ Buffer data = new Buffer();
+ data.writeUtf8(original);
+ Buffer sink = new Buffer();
+ DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
+ deflaterSink.write(data, data.size());
+ deflaterSink.flush();
+ Buffer inflated = inflate(sink);
+ assertEquals(original, inflated.readUtf8());
+ }
+
+ @Test public void deflateWellCompressed() throws IOException {
+ String original = repeat("a", 1024 * 1024);
+ Buffer data = new Buffer();
+ data.writeUtf8(original);
+ Buffer sink = new Buffer();
+ DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
+ deflaterSink.write(data, data.size());
+ deflaterSink.close();
+ Buffer inflated = inflate(sink);
+ assertEquals(original, inflated.readUtf8());
+ }
+
+ @Test public void deflatePoorlyCompressed() throws IOException {
+ ByteString original = randomBytes(1024 * 1024);
+ Buffer data = new Buffer();
+ data.write(original);
+ Buffer sink = new Buffer();
+ DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
+ deflaterSink.write(data, data.size());
+ deflaterSink.close();
+ Buffer inflated = inflate(sink);
+ assertEquals(original, inflated.readByteString());
+ }
+
+ @Test public void multipleSegmentsWithoutCompression() throws IOException {
+ Buffer buffer = new Buffer();
+ Deflater deflater = new Deflater();
+ deflater.setLevel(Deflater.NO_COMPRESSION);
+ DeflaterSink deflaterSink = new DeflaterSink(buffer, deflater);
+ int byteCount = SEGMENT_SIZE * 4;
+ deflaterSink.write(new Buffer().writeUtf8(repeat("a", byteCount)), byteCount);
+ deflaterSink.close();
+ assertEquals(repeat("a", byteCount), inflate(buffer).readUtf8(byteCount));
+ }
+
+ @Test public void deflateIntoNonemptySink() throws Exception {
+ String original = "They're moving in herds. They do move in herds.";
+
+ // Exercise all possible offsets for the outgoing segment.
+ for (int i = 0; i < SEGMENT_SIZE; i++) {
+ Buffer data = new Buffer().writeUtf8(original);
+ Buffer sink = new Buffer().writeUtf8(repeat("a", i));
+
+ DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
+ deflaterSink.write(data, data.size());
+ deflaterSink.close();
+
+ sink.skip(i);
+ Buffer inflated = inflate(sink);
+ assertEquals(original, inflated.readUtf8());
+ }
+ }
+
+ /**
+ * This test deflates a single segment of without compression because that's
+ * the easiest way to force close() to emit a large amount of data to the
+ * underlying sink.
+ */
+ @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException {
+ MockSink mockSink = new MockSink();
+ mockSink.scheduleThrow(0, new IOException("first"));
+ mockSink.scheduleThrow(1, new IOException("second"));
+ Deflater deflater = new Deflater();
+ deflater.setLevel(Deflater.NO_COMPRESSION);
+ DeflaterSink deflaterSink = new DeflaterSink(mockSink, deflater);
+ deflaterSink.write(new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE)), SEGMENT_SIZE);
+ try {
+ deflaterSink.close();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("first", expected.getMessage());
+ }
+ mockSink.assertLogContains("close()");
+ }
+
+ /**
+ * Uses streaming decompression to inflate {@code deflated}. The input must
+ * either be finished or have a trailing sync flush.
+ */
+ private Buffer inflate(Buffer deflated) throws IOException {
+ InputStream deflatedIn = deflated.inputStream();
+ Inflater inflater = new Inflater();
+ InputStream inflatedIn = new InflaterInputStream(deflatedIn, inflater);
+ Buffer result = new Buffer();
+ byte[] buffer = new byte[8192];
+ while (!inflater.needsInput() || deflated.size() > 0 || deflatedIn.available() > 0) {
+ int count = inflatedIn.read(buffer, 0, buffer.length);
+ if (count != -1) {
+ result.write(buffer, 0, count);
+ }
+ }
+ return result;
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/ForwardingTimeoutTest.java b/okio/src/jvmTest/java/okio/ForwardingTimeoutTest.java
new file mode 100644
index 00000000..45536fc0
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/ForwardingTimeoutTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ForwardingTimeoutTest {
+ @Test public void getAndSetDelegate() {
+ Timeout timeout1 = new Timeout();
+ Timeout timeout2 = new Timeout();
+
+ ForwardingTimeout forwardingTimeout = new ForwardingTimeout(timeout1);
+ forwardingTimeout.timeout(5, TimeUnit.SECONDS);
+ assertThat(timeout1.timeoutNanos()).isNotEqualTo(0L);
+ assertThat(timeout2.timeoutNanos()).isEqualTo(0L);
+ forwardingTimeout.clearTimeout();
+ assertThat(timeout1.timeoutNanos()).isEqualTo(0L);
+ assertThat(timeout2.timeoutNanos()).isEqualTo(0L);
+ assertThat(forwardingTimeout.delegate()).isEqualTo(timeout1);
+
+ assertThat(forwardingTimeout.setDelegate(timeout2)).isEqualTo(forwardingTimeout);
+ forwardingTimeout.timeout(5, TimeUnit.SECONDS);
+ assertThat(timeout1.timeoutNanos()).isEqualTo(0L);
+ assertThat(timeout2.timeoutNanos()).isNotEqualTo(0L);
+ forwardingTimeout.clearTimeout();
+ assertThat(timeout1.timeoutNanos()).isEqualTo(0L);
+ assertThat(timeout2.timeoutNanos()).isEqualTo(0L);
+ assertThat(forwardingTimeout.delegate()).isEqualTo(timeout2);
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/GzipSinkTest.java b/okio/src/jvmTest/java/okio/GzipSinkTest.java
new file mode 100644
index 00000000..848ff02c
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/GzipSinkTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import org.junit.Test;
+
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class GzipSinkTest {
+ @Test public void gzipGunzip() throws Exception {
+ Buffer data = new Buffer();
+ String original = "It's a UNIX system! I know this!";
+ data.writeUtf8(original);
+ Buffer sink = new Buffer();
+ GzipSink gzipSink = new GzipSink(sink);
+ gzipSink.write(data, data.size());
+ gzipSink.close();
+ Buffer inflated = gunzip(sink);
+ assertEquals(original, inflated.readUtf8());
+ }
+
+ @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException {
+ MockSink mockSink = new MockSink();
+ mockSink.scheduleThrow(0, new IOException("first"));
+ mockSink.scheduleThrow(1, new IOException("second"));
+ GzipSink gzipSink = new GzipSink(mockSink);
+ gzipSink.write(new Buffer().writeUtf8(repeat("a", SEGMENT_SIZE)), SEGMENT_SIZE);
+ try {
+ gzipSink.close();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("first", expected.getMessage());
+ }
+ mockSink.assertLogContains("close()");
+ }
+
+ private Buffer gunzip(Buffer gzipped) throws IOException {
+ Buffer result = new Buffer();
+ GzipSource source = new GzipSource(gzipped);
+ while (source.read(result, Integer.MAX_VALUE) != -1) {
+ }
+ return result;
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/GzipSourceTest.java b/okio/src/jvmTest/java/okio/GzipSourceTest.java
new file mode 100644
index 00000000..69b81e3f
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/GzipSourceTest.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.util.zip.CRC32;
+import org.junit.Test;
+
+import static kotlin.text.Charsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class GzipSourceTest {
+
+ @Test public void gunzip() throws Exception {
+ Buffer gzipped = new Buffer();
+ gzipped.write(gzipHeader);
+ gzipped.write(deflated);
+ gzipped.write(gzipTrailer);
+ assertGzipped(gzipped);
+ }
+
+ @Test public void gunzip_withHCRC() throws Exception {
+ CRC32 hcrc = new CRC32();
+ ByteString gzipHeader = gzipHeaderWithFlags((byte) 0x02);
+ hcrc.update(gzipHeader.toByteArray());
+
+ Buffer gzipped = new Buffer();
+ gzipped.write(gzipHeader);
+ gzipped.writeShort(TestUtil.reverseBytes((short) hcrc.getValue())); // little endian
+ gzipped.write(deflated);
+ gzipped.write(gzipTrailer);
+ assertGzipped(gzipped);
+ }
+
+ @Test public void gunzip_withExtra() throws Exception {
+ Buffer gzipped = new Buffer();
+ gzipped.write(gzipHeaderWithFlags((byte) 0x04));
+ gzipped.writeShort(TestUtil.reverseBytes((short) 7)); // little endian extra length
+ gzipped.write("blubber".getBytes(UTF_8), 0, 7);
+ gzipped.write(deflated);
+ gzipped.write(gzipTrailer);
+ assertGzipped(gzipped);
+ }
+
+ @Test public void gunzip_withName() throws Exception {
+ Buffer gzipped = new Buffer();
+ gzipped.write(gzipHeaderWithFlags((byte) 0x08));
+ gzipped.write("foo.txt".getBytes(UTF_8), 0, 7);
+ gzipped.writeByte(0); // zero-terminated
+ gzipped.write(deflated);
+ gzipped.write(gzipTrailer);
+ assertGzipped(gzipped);
+ }
+
+ @Test public void gunzip_withComment() throws Exception {
+ Buffer gzipped = new Buffer();
+ gzipped.write(gzipHeaderWithFlags((byte) 0x10));
+ gzipped.write("rubbish".getBytes(UTF_8), 0, 7);
+ gzipped.writeByte(0); // zero-terminated
+ gzipped.write(deflated);
+ gzipped.write(gzipTrailer);
+ assertGzipped(gzipped);
+ }
+
+ /**
+ * For portability, it is a good idea to export the gzipped bytes and try running gzip. Ex.
+ * {@code echo gzipped | base64 --decode | gzip -l -v}
+ */
+ @Test public void gunzip_withAll() throws Exception {
+ Buffer gzipped = new Buffer();
+ gzipped.write(gzipHeaderWithFlags((byte) 0x1c));
+ gzipped.writeShort(TestUtil.reverseBytes((short) 7)); // little endian extra length
+ gzipped.write("blubber".getBytes(UTF_8), 0, 7);
+ gzipped.write("foo.txt".getBytes(UTF_8), 0, 7);
+ gzipped.writeByte(0); // zero-terminated
+ gzipped.write("rubbish".getBytes(UTF_8), 0, 7);
+ gzipped.writeByte(0); // zero-terminated
+ gzipped.write(deflated);
+ gzipped.write(gzipTrailer);
+ assertGzipped(gzipped);
+ }
+
+ private void assertGzipped(Buffer gzipped) throws IOException {
+ Buffer gunzipped = gunzip(gzipped);
+ assertEquals("It's a UNIX system! I know this!", gunzipped.readUtf8());
+ }
+
+ /**
+ * Note that you cannot test this with old versions of gzip, as they interpret flag bit 1 as
+ * CONTINUATION, not HCRC. For example, this is the case with the default gzip on osx.
+ */
+ @Test public void gunzipWhenHeaderCRCIncorrect() {
+ Buffer gzipped = new Buffer();
+ gzipped.write(gzipHeaderWithFlags((byte) 0x02));
+ gzipped.writeShort((short) 0); // wrong HCRC!
+ gzipped.write(deflated);
+ gzipped.write(gzipTrailer);
+
+ try {
+ gunzip(gzipped);
+ fail();
+ } catch (IOException e) {
+ assertEquals("FHCRC: actual 0x0000261d != expected 0x00000000", e.getMessage());
+ }
+ }
+
+ @Test public void gunzipWhenCRCIncorrect() {
+ Buffer gzipped = new Buffer();
+ gzipped.write(gzipHeader);
+ gzipped.write(deflated);
+ gzipped.writeInt(TestUtil.reverseBytes(0x1234567)); // wrong CRC
+ gzipped.write(gzipTrailer.toByteArray(), 3, 4);
+
+ try {
+ gunzip(gzipped);
+ fail();
+ } catch (IOException e) {
+ assertEquals("CRC: actual 0x37ad8f8d != expected 0x01234567", e.getMessage());
+ }
+ }
+
+ @Test public void gunzipWhenLengthIncorrect() {
+ Buffer gzipped = new Buffer();
+ gzipped.write(gzipHeader);
+ gzipped.write(deflated);
+ gzipped.write(gzipTrailer.toByteArray(), 0, 4);
+ gzipped.writeInt(TestUtil.reverseBytes(0x123456)); // wrong length
+
+ try {
+ gunzip(gzipped);
+ fail();
+ } catch (IOException e) {
+ assertEquals("ISIZE: actual 0x00000020 != expected 0x00123456", e.getMessage());
+ }
+ }
+
+ @Test public void gunzipExhaustsSource() throws Exception {
+ Buffer gzippedSource = new Buffer()
+ .write(ByteString.decodeHex("1f8b08000000000000004b4c4a0600c241243503000000")); // 'abc'
+
+ ExhaustableSource exhaustableSource = new ExhaustableSource(gzippedSource);
+ BufferedSource gunzippedSource = Okio.buffer(new GzipSource(exhaustableSource));
+
+ assertEquals('a', gunzippedSource.readByte());
+ assertEquals('b', gunzippedSource.readByte());
+ assertEquals('c', gunzippedSource.readByte());
+ assertFalse(exhaustableSource.exhausted);
+ assertEquals(-1, gunzippedSource.read(new Buffer(), 1));
+ assertTrue(exhaustableSource.exhausted);
+ }
+
+ @Test public void gunzipThrowsIfSourceIsNotExhausted() throws Exception {
+ Buffer gzippedSource = new Buffer()
+ .write(ByteString.decodeHex("1f8b08000000000000004b4c4a0600c241243503000000")); // 'abc'
+ gzippedSource.writeByte('d'); // This byte shouldn't be here!
+
+ BufferedSource gunzippedSource = Okio.buffer(new GzipSource(gzippedSource));
+
+ assertEquals('a', gunzippedSource.readByte());
+ assertEquals('b', gunzippedSource.readByte());
+ assertEquals('c', gunzippedSource.readByte());
+ try {
+ gunzippedSource.readByte();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ private ByteString gzipHeaderWithFlags(byte flags) {
+ byte[] result = gzipHeader.toByteArray();
+ result[3] = flags;
+ return ByteString.of(result);
+ }
+
+ private final ByteString gzipHeader = ByteString.decodeHex("1f8b0800000000000000");
+
+ // Deflated "It's a UNIX system! I know this!"
+ private final ByteString deflated = ByteString.decodeHex(
+ "f32c512f56485408f5f38c5028ae2c2e49cd5554f054c8cecb2f5728c9c82c560400");
+
+ private final ByteString gzipTrailer = ByteString.decodeHex(""
+ + "8d8fad37" // Checksum of deflated.
+ + "20000000" // 32 in little endian.
+ );
+
+ private Buffer gunzip(Buffer gzipped) throws IOException {
+ Buffer result = new Buffer();
+ GzipSource source = new GzipSource(gzipped);
+ while (source.read(result, Integer.MAX_VALUE) != -1) {
+ }
+ return result;
+ }
+
+ /** This source keeps track of whether its read has returned -1. */
+ static class ExhaustableSource implements Source {
+ private final Source source;
+ private boolean exhausted;
+
+ ExhaustableSource(Source source) {
+ this.source = source;
+ }
+
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ long result = source.read(sink, byteCount);
+ if (result == -1) exhausted = true;
+ return result;
+ }
+
+ @Override public Timeout timeout() {
+ return source.timeout();
+ }
+
+ @Override public void close() throws IOException {
+ source.close();
+ }
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/InflaterSourceTest.java b/okio/src/jvmTest/java/okio/InflaterSourceTest.java
new file mode 100644
index 00000000..0486638d
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/InflaterSourceTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.List;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.Inflater;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static okio.TestUtil.randomBytes;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+
+@RunWith(Parameterized.class)
+public final class InflaterSourceTest {
+ /**
+ * Use a parameterized test to control how many bytes the InflaterSource gets with each request
+ * for more bytes.
+ */
+ @Parameters(name = "{0}")
+ public static List<Object[]> parameters() {
+ return BufferedSourceFactory.Companion.getPARAMETERIZED_TEST_VALUES();
+ }
+
+ public final BufferedSourceFactory bufferFactory;
+ public BufferedSink deflatedSink;
+ public BufferedSource deflatedSource;
+
+ public InflaterSourceTest(BufferedSourceFactory bufferFactory) {
+ this.bufferFactory = bufferFactory;
+ resetDeflatedSourceAndSink();
+ }
+
+ private void resetDeflatedSourceAndSink() {
+ BufferedSourceFactory.Pipe pipe = bufferFactory.pipe();
+ this.deflatedSink = pipe.getSink();
+ this.deflatedSource = pipe.getSource();
+ }
+
+ @Test public void inflate() throws Exception {
+ decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CD5s=");
+ Buffer inflated = inflate(deflatedSource);
+ assertEquals("God help us, we're in the hands of engineers.", inflated.readUtf8());
+ }
+
+ @Test public void inflateTruncated() throws Exception {
+ decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CDw==");
+ try {
+ inflate(deflatedSource);
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void inflateWellCompressed() throws Exception {
+ decodeBase64("eJztwTEBAAAAwqCs61/CEL5AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8BtFeWvE=");
+ String original = repeat("a", 1024 * 1024);
+ deflate(ByteString.encodeUtf8(original));
+ Buffer inflated = inflate(deflatedSource);
+ assertEquals(original, inflated.readUtf8());
+ }
+
+ @Test public void inflatePoorlyCompressed() throws Exception {
+ assumeFalse(bufferFactory.isOneByteAtATime()); // 8 GiB for 1 byte per segment!
+
+ ByteString original = randomBytes(1024 * 1024);
+ deflate(original);
+ Buffer inflated = inflate(deflatedSource);
+ assertEquals(original, inflated.readByteString());
+ }
+
+ @Test public void inflateIntoNonemptySink() throws Exception {
+ for (int i = 0; i < SEGMENT_SIZE; i++) {
+ resetDeflatedSourceAndSink();
+ Buffer inflated = new Buffer().writeUtf8(repeat("a", i));
+ deflate(ByteString.encodeUtf8("God help us, we're in the hands of engineers."));
+ InflaterSource source = new InflaterSource(deflatedSource, new Inflater());
+ while (source.read(inflated, Integer.MAX_VALUE) != -1) {
+ }
+ inflated.skip(i);
+ assertEquals("God help us, we're in the hands of engineers.", inflated.readUtf8());
+ }
+ }
+
+ @Test public void inflateSingleByte() throws Exception {
+ Buffer inflated = new Buffer();
+ decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CD5s=");
+ InflaterSource source = new InflaterSource(deflatedSource, new Inflater());
+ source.read(inflated, 1);
+ source.close();
+ assertEquals("G", inflated.readUtf8());
+ assertEquals(0, inflated.size());
+ }
+
+ @Test public void inflateByteCount() throws Exception {
+ assumeFalse(bufferFactory.isOneByteAtATime()); // This test assumes one step.
+
+ Buffer inflated = new Buffer();
+ decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tKtYDAF6CD5s=");
+ InflaterSource source = new InflaterSource(deflatedSource, new Inflater());
+ source.read(inflated, 11);
+ source.close();
+ assertEquals("God help us", inflated.readUtf8());
+ assertEquals(0, inflated.size());
+ }
+
+ @Test public void sourceExhaustedPrematurelyOnRead() throws Exception {
+ // Deflate 0 bytes of data that lacks the in-stream terminator.
+ decodeBase64("eJwAAAD//w==");
+
+ Buffer inflated = new Buffer();
+ Inflater inflater = new Inflater();
+ InflaterSource source = new InflaterSource(deflatedSource, inflater);
+ assertThat(deflatedSource.exhausted()).isFalse();
+ try {
+ source.read(inflated, Long.MAX_VALUE);
+ fail();
+ } catch (EOFException expected) {
+ assertThat(expected).hasMessage("source exhausted prematurely");
+ }
+
+ // Despite the exception, the read() call made forward progress on the underlying stream!
+ assertThat(deflatedSource.exhausted()).isTrue();
+ }
+
+ /**
+ * Confirm that {@link InflaterSource#readOrInflate} consumes a byte on each call even if it
+ * doesn't produce a byte on every call.
+ */
+ @Test public void readOrInflateMakesByteByByteProgress() throws Exception {
+ // Deflate 0 bytes of data that lacks the in-stream terminator.
+ decodeBase64("eJwAAAD//w==");
+ int deflatedByteCount = 7;
+
+ Buffer inflated = new Buffer();
+ Inflater inflater = new Inflater();
+ InflaterSource source = new InflaterSource(deflatedSource, inflater);
+ assertThat(deflatedSource.exhausted()).isFalse();
+
+ if (bufferFactory.isOneByteAtATime()) {
+ for (int i = 0; i < deflatedByteCount; i++) {
+ assertThat(inflater.getBytesRead()).isEqualTo(i);
+ assertThat(source.readOrInflate(inflated, Long.MAX_VALUE)).isEqualTo(0L);
+ }
+ } else {
+ assertThat(source.readOrInflate(inflated, Long.MAX_VALUE)).isEqualTo(0L);
+ }
+
+ assertThat(inflater.getBytesRead()).isEqualTo(deflatedByteCount);
+ assertThat(deflatedSource.exhausted());
+ }
+
+ private void decodeBase64(String s) throws IOException {
+ deflatedSink.write(ByteString.decodeBase64(s));
+ deflatedSink.flush();
+ }
+
+ /** Use DeflaterOutputStream to deflate source. */
+ private void deflate(ByteString source) throws IOException {
+ Sink sink = Okio.sink(new DeflaterOutputStream(deflatedSink.outputStream()));
+ sink.write(new Buffer().write(source), source.size());
+ sink.close();
+ }
+
+ /** Returns a new buffer containing the inflated contents of {@code deflated}. */
+ private Buffer inflate(BufferedSource deflated) throws IOException {
+ Buffer result = new Buffer();
+ InflaterSource source = new InflaterSource(deflated, new Inflater());
+ while (source.read(result, Integer.MAX_VALUE) != -1) {
+ }
+ return result;
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/LargeStreamsTest.java b/okio/src/jvmTest/java/okio/LargeStreamsTest.java
new file mode 100644
index 00000000..b9be1a2e
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/LargeStreamsTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.zip.Deflater;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+import org.junit.Test;
+
+import static okio.TestUtil.SEGMENT_SIZE;
+import static okio.TestUtil.randomSource;
+import static org.junit.Assert.assertEquals;
+
+/** Slow running tests that run a large amount of data through a stream. */
+public final class LargeStreamsTest {
+ /** 4 GiB plus 1 byte. This is greater than what can be expressed in an unsigned int. */
+ public static final long FOUR_GIB_PLUS_ONE = 0x100000001L;
+
+ /** SHA-256 of {@code TestUtil.randomSource(FOUR_GIB_PLUS_ONE)}. */
+ public static final ByteString SHA256_RANDOM_FOUR_GIB_PLUS_1 = ByteString.decodeHex(
+ "9654947a655c5efc445502fd1bf11117d894b7812b7974fde8ca4a02c5066315");
+
+ @Test public void test() throws Exception {
+ Pipe pipe = new Pipe(1024 * 1024);
+
+ Future<Long> future = readAllAndCloseAsync(randomSource(FOUR_GIB_PLUS_ONE), pipe.sink());
+
+ HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());
+ readAllAndClose(pipe.source(), hashingSink);
+
+ assertEquals(FOUR_GIB_PLUS_ONE, (long) future.get());
+ assertEquals(SHA256_RANDOM_FOUR_GIB_PLUS_1, hashingSink.hash());
+ }
+
+ /** Note that this test hangs on Android. */
+ @Test public void gzipSource() throws Exception {
+ Pipe pipe = new Pipe(1024 * 1024);
+
+ OutputStream gzipOut = new GZIPOutputStream(Okio.buffer(pipe.sink()).outputStream()) {
+ {
+ // Disable compression to speed up a slow test. Improved from 141s to 33s on one machine.
+ def.setLevel(Deflater.NO_COMPRESSION);
+ }
+ };
+ Future<Long> future = readAllAndCloseAsync(
+ randomSource(FOUR_GIB_PLUS_ONE), Okio.sink(gzipOut));
+
+ HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());
+ GzipSource gzipSource = new GzipSource(pipe.source());
+ readAllAndClose(gzipSource, hashingSink);
+
+ assertEquals(FOUR_GIB_PLUS_ONE, (long) future.get());
+ assertEquals(SHA256_RANDOM_FOUR_GIB_PLUS_1, hashingSink.hash());
+ }
+
+ /** Note that this test hangs on Android. */
+ @Test public void gzipSink() throws Exception {
+ Pipe pipe = new Pipe(1024 * 1024);
+
+ GzipSink gzipSink = new GzipSink(pipe.sink());
+
+ // Disable compression to speed up a slow test. Improved from 141s to 35s on one machine.
+ gzipSink.deflater().setLevel(Deflater.NO_COMPRESSION);
+ Future<Long> future = readAllAndCloseAsync(randomSource(FOUR_GIB_PLUS_ONE), gzipSink);
+
+ HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());
+ GZIPInputStream gzipIn = new GZIPInputStream(Okio.buffer(pipe.source()).inputStream());
+ readAllAndClose(Okio.source(gzipIn), hashingSink);
+
+ assertEquals(FOUR_GIB_PLUS_ONE, (long) future.get());
+ assertEquals(SHA256_RANDOM_FOUR_GIB_PLUS_1, hashingSink.hash());
+ }
+
+ /** Reads all bytes from {@code source} and writes them to {@code sink}. */
+ private Long readAllAndClose(Source source, Sink sink) throws IOException {
+ long result = 0L;
+ Buffer buffer = new Buffer();
+ for (long count; (count = source.read(buffer, SEGMENT_SIZE)) != -1L; result += count) {
+ sink.write(buffer, count);
+ }
+ source.close();
+ sink.close();
+ return result;
+ }
+
+ /** Calls {@link #readAllAndClose} on a background thread. */
+ private Future<Long> readAllAndCloseAsync(final Source source, final Sink sink) {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ return executor.submit(new Callable<Long>() {
+ @Override public Long call() throws Exception {
+ return readAllAndClose(source, sink);
+ }
+ });
+ } finally {
+ executor.shutdown();
+ }
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/MessageDigestConsistencyTest.kt b/okio/src/jvmTest/java/okio/MessageDigestConsistencyTest.kt
new file mode 100644
index 00000000..962d0119
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/MessageDigestConsistencyTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.ByteString.Companion.toByteString
+import okio.internal.HashFunction
+import okio.internal.Md5
+import okio.internal.Sha1
+import okio.internal.Sha256
+import okio.internal.Sha512
+import org.assertj.core.api.Assertions.assertThat
+import java.security.MessageDigest
+import java.util.Random
+import kotlin.test.Test
+
+/**
+ * Confirm Okio is consistent with the JDK's MessageDigest algorithms for various sizes and slices.
+ * This makes repeated calls to update() with byte arrays of various sizes and contents to defend
+ * against bugs in batching inputs.
+ */
+class MessageDigestConsistencyTest {
+ @Test fun sha1() {
+ test("SHA-1") { Sha1() }
+ }
+
+ @Test fun sha256() {
+ test("SHA-256") { Sha256() }
+ }
+
+ @Test fun sha512() {
+ test("SHA-512") { Sha512() }
+ }
+
+ @Test fun md5() {
+ test("MD5") { Md5() }
+ }
+
+ private fun test(algorithm: String, newHashFunction: () -> HashFunction) {
+ for (seed in 0L until 1000L) {
+ for (updateCount in 0 until 10) {
+ test(
+ algorithm = algorithm,
+ hashFunction = newHashFunction(),
+ seed = seed,
+ updateCount = updateCount
+ )
+ }
+ }
+ }
+
+ private fun test(
+ algorithm: String,
+ hashFunction: HashFunction,
+ seed: Long,
+ updateCount: Int
+ ) {
+ val data = Buffer()
+
+ val random = Random(seed)
+ for (i in 0 until updateCount) {
+ val size = random.nextInt(1000) + 1 // size must be >= 1.
+ val byteArray = ByteArray(size).also { random.nextBytes(it) }
+ val offset = random.nextInt(size)
+ val byteCount = random.nextInt(size - offset)
+
+ hashFunction.update(
+ input = byteArray,
+ offset = offset,
+ byteCount = byteCount
+ )
+
+ data.write(
+ source = byteArray,
+ offset = offset,
+ byteCount = byteCount
+ )
+ }
+
+ val okioHash = hashFunction.digest()
+
+ val byteArray = data.readByteArray()
+ val jdkMessageDigest = MessageDigest.getInstance(algorithm)
+ val jdkHash = jdkMessageDigest.digest(byteArray)
+
+ assertThat(okioHash.toByteString()).isEqualTo(jdkHash.toByteString())
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/NioTest.java b/okio/src/jvmTest/java/okio/NioTest.java
new file mode 100644
index 00000000..aec17733
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/NioTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.file.StandardOpenOption;
+import kotlin.text.Charsets;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+
+/** Test interop between our beloved Okio and java.nio. */
+public final class NioTest {
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test public void sourceIsOpen() throws Exception {
+ BufferedSource source = Okio.buffer((Source) new Buffer());
+ assertTrue(source.isOpen());
+ source.close();
+ assertFalse(source.isOpen());
+ }
+
+ @Test public void sinkIsOpen() throws Exception {
+ BufferedSink sink = Okio.buffer((Sink) new Buffer());
+ assertTrue(sink.isOpen());
+ sink.close();
+ assertFalse(sink.isOpen());
+ }
+
+ @Test public void writableChannelNioFile() throws Exception {
+ File file = temporaryFolder.newFile();
+ FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.WRITE);
+ testWritableByteChannel(fileChannel);
+
+ BufferedSource emitted = Okio.buffer(Okio.source(file));
+ assertEquals("defghijklmnopqrstuvw", emitted.readUtf8());
+ emitted.close();
+ }
+
+ @Test public void writableChannelBuffer() throws Exception {
+ Buffer buffer = new Buffer();
+ testWritableByteChannel(buffer);
+ assertEquals("defghijklmnopqrstuvw", buffer.readUtf8());
+ }
+
+ @Test public void writableChannelBufferedSink() throws Exception {
+ Buffer buffer = new Buffer();
+ BufferedSink bufferedSink = Okio.buffer((Sink) buffer);
+ testWritableByteChannel(bufferedSink);
+ assertEquals("defghijklmnopqrstuvw", buffer.readUtf8());
+ }
+
+ @Test public void readableChannelNioFile() throws Exception {
+ File file = temporaryFolder.newFile();
+
+ BufferedSink initialData = Okio.buffer(Okio.sink(file));
+ initialData.writeUtf8("abcdefghijklmnopqrstuvwxyz");
+ initialData.close();
+
+ FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ);
+ testReadableByteChannel(fileChannel);
+ }
+
+ @Test public void readableChannelBuffer() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8("abcdefghijklmnopqrstuvwxyz");
+
+ testReadableByteChannel(buffer);
+ }
+
+ @Test public void readableChannelBufferedSource() throws Exception {
+ Buffer buffer = new Buffer();
+ BufferedSource bufferedSource = Okio.buffer((Source) buffer);
+ buffer.writeUtf8("abcdefghijklmnopqrstuvwxyz");
+
+ testReadableByteChannel(bufferedSource);
+ }
+
+ /**
+ * Does some basic writes to {@code channel}. We execute this against both Okio's channels and
+ * also a standard implementation from the JDK to confirm that their behavior is consistent.
+ */
+ private void testWritableByteChannel(WritableByteChannel channel) throws Exception {
+ assertTrue(channel.isOpen());
+
+ ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
+ byteBuffer.put("abcdefghijklmnopqrstuvwxyz".getBytes(Charsets.UTF_8));
+ byteBuffer.flip();
+ byteBuffer.position(3);
+ byteBuffer.limit(23);
+
+ int byteCount = channel.write(byteBuffer);
+ assertEquals(20, byteCount);
+ assertEquals(23, byteBuffer.position());
+ assertEquals(23, byteBuffer.limit());
+
+ channel.close();
+ assertEquals(channel instanceof Buffer, channel.isOpen()); // Buffer.close() does nothing.
+ }
+
+ /**
+ * Does some basic reads from {@code channel}. We execute this against both Okio's channels and
+ * also a standard implementation from the JDK to confirm that their behavior is consistent.
+ */
+ private void testReadableByteChannel(ReadableByteChannel channel) throws Exception {
+ assertTrue(channel.isOpen());
+
+ ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
+ byteBuffer.position(3);
+ byteBuffer.limit(23);
+
+ int byteCount = channel.read(byteBuffer);
+ assertEquals(20, byteCount);
+ assertEquals(23, byteBuffer.position());
+ assertEquals(23, byteBuffer.limit());
+
+ channel.close();
+ assertEquals(channel instanceof Buffer, channel.isOpen()); // Buffer.close() does nothing.
+
+ byteBuffer.flip();
+ byteBuffer.position(3);
+ byte[] data = new byte[byteBuffer.remaining()];
+ byteBuffer.get(data);
+ assertEquals("abcdefghijklmnopqrst", new String(data, Charsets.UTF_8));
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/OkioTest.java b/okio/src/jvmTest/java/okio/OkioTest.java
new file mode 100644
index 00000000..71a44470
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/OkioTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static kotlin.text.Charsets.UTF_8;
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static okio.TestUtil.assertNoEmptySegments;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class OkioTest {
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test public void readWriteFile() throws Exception {
+ File file = temporaryFolder.newFile();
+
+ BufferedSink sink = Okio.buffer(Okio.sink(file));
+ sink.writeUtf8("Hello, java.io file!");
+ sink.close();
+ assertTrue(file.exists());
+ assertEquals(20, file.length());
+
+ BufferedSource source = Okio.buffer(Okio.source(file));
+ assertEquals("Hello, java.io file!", source.readUtf8());
+ source.close();
+ }
+
+ @Test public void appendFile() throws Exception {
+ File file = temporaryFolder.newFile();
+
+ BufferedSink sink = Okio.buffer(Okio.appendingSink(file));
+ sink.writeUtf8("Hello, ");
+ sink.close();
+ assertTrue(file.exists());
+ assertEquals(7, file.length());
+
+ sink = Okio.buffer(Okio.appendingSink(file));
+ sink.writeUtf8("java.io file!");
+ sink.close();
+ assertEquals(20, file.length());
+
+ BufferedSource source = Okio.buffer(Okio.source(file));
+ assertEquals("Hello, java.io file!", source.readUtf8());
+ source.close();
+ }
+
+ @Test public void readWritePath() throws Exception {
+ Path path = temporaryFolder.newFile().toPath();
+
+ BufferedSink sink = Okio.buffer(Okio.sink(path));
+ sink.writeUtf8("Hello, java.nio file!");
+ sink.close();
+ assertTrue(Files.exists(path));
+ assertEquals(21, Files.size(path));
+
+ BufferedSource source = Okio.buffer(Okio.source(path));
+ assertEquals("Hello, java.nio file!", source.readUtf8());
+ source.close();
+ }
+
+ @Test public void sinkFromOutputStream() throws Exception {
+ Buffer data = new Buffer();
+ data.writeUtf8("a");
+ data.writeUtf8(repeat("b", 9998));
+ data.writeUtf8("c");
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Sink sink = Okio.sink(out);
+ sink.write(data, 3);
+ assertEquals("abb", out.toString("UTF-8"));
+ sink.write(data, data.size());
+ assertEquals("a" + repeat("b", 9998) + "c", out.toString("UTF-8"));
+ }
+
+ @Test public void sourceFromInputStream() throws Exception {
+ InputStream in = new ByteArrayInputStream(
+ ("a" + repeat("b", SEGMENT_SIZE * 2) + "c").getBytes(UTF_8));
+
+ // Source: ab...bc
+ Source source = Okio.source(in);
+ Buffer sink = new Buffer();
+
+ // Source: b...bc. Sink: abb.
+ assertEquals(3, source.read(sink, 3));
+ assertEquals("abb", sink.readUtf8(3));
+
+ // Source: b...bc. Sink: b...b.
+ assertEquals(SEGMENT_SIZE, source.read(sink, 20000));
+ assertEquals(repeat("b", SEGMENT_SIZE), sink.readUtf8());
+
+ // Source: b...bc. Sink: b...bc.
+ assertEquals(SEGMENT_SIZE - 1, source.read(sink, 20000));
+ assertEquals(repeat("b", SEGMENT_SIZE - 2) + "c", sink.readUtf8());
+
+ // Source and sink are empty.
+ assertEquals(-1, source.read(sink, 1));
+ }
+
+ @Test public void sourceFromInputStreamWithSegmentSize() throws Exception {
+ InputStream in = new ByteArrayInputStream(new byte[SEGMENT_SIZE]);
+ Source source = Okio.source(in);
+ Buffer sink = new Buffer();
+
+ assertEquals(SEGMENT_SIZE, source.read(sink, SEGMENT_SIZE));
+ assertEquals(-1, source.read(sink, SEGMENT_SIZE));
+
+ assertNoEmptySegments(sink);
+ }
+
+ @Test public void sourceFromInputStreamBounds() throws Exception {
+ Source source = Okio.source(new ByteArrayInputStream(new byte[100]));
+ try {
+ source.read(new Buffer(), -1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void bufferSinkThrowsOnNull() {
+ try {
+ Okio.buffer((Sink) null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test public void bufferSourceThrowsOnNull() {
+ try {
+ Okio.buffer((Source) null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test public void blackhole() throws Exception {
+ Buffer data = new Buffer();
+ data.writeUtf8("blackhole");
+
+ Sink blackhole = Okio.blackhole();
+ blackhole.write(data, 5);
+
+ assertEquals("hole", data.readUtf8());
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/PipeTest.java b/okio/src/jvmTest/java/okio/PipeTest.java
new file mode 100644
index 00000000..030e6ba0
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/PipeTest.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class PipeTest {
+ final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
+
+ @After public void tearDown() throws Exception {
+ executorService.shutdown();
+ }
+
+ @Test public void test() throws Exception {
+ Pipe pipe = new Pipe(6);
+ pipe.sink().write(new Buffer().writeUtf8("abc"), 3L);
+
+ Source source = pipe.source();
+ Buffer readBuffer = new Buffer();
+ assertEquals(3L, source.read(readBuffer, 6L));
+ assertEquals("abc", readBuffer.readUtf8());
+
+ pipe.sink().close();
+ assertEquals(-1L, source.read(readBuffer, 6L));
+
+ source.close();
+ }
+
+ /**
+ * A producer writes the first 16 MiB of bytes generated by {@code new Random(0)} to a sink, and a
+ * consumer consumes them. Both compute hashes of their data to confirm that they're as expected.
+ */
+ @Test public void largeDataset() throws Exception {
+ final Pipe pipe = new Pipe(1000L); // An awkward size to force producer/consumer exchange.
+ final long totalBytes = 16L * 1024L * 1024L;
+ ByteString expectedHash = ByteString.decodeHex("7c3b224bea749086babe079360cf29f98d88262d");
+
+ // Write data to the sink.
+ Future<ByteString> sinkHash = executorService.submit(new Callable<ByteString>() {
+ @Override public ByteString call() throws Exception {
+ HashingSink hashingSink = HashingSink.sha1(pipe.sink());
+ Random random = new Random(0);
+ byte[] data = new byte[8192];
+
+ Buffer buffer = new Buffer();
+ for (long i = 0L; i < totalBytes; i += data.length) {
+ random.nextBytes(data);
+ buffer.write(data);
+ hashingSink.write(buffer, buffer.size());
+ }
+
+ hashingSink.close();
+ return hashingSink.hash();
+ }
+ });
+
+ // Read data from the source.
+ Future<ByteString> sourceHash = executorService.submit(new Callable<ByteString>() {
+ @Override public ByteString call() throws Exception {
+ Buffer blackhole = new Buffer();
+ HashingSink hashingSink = HashingSink.sha1(blackhole);
+
+ Buffer buffer = new Buffer();
+ while (pipe.source().read(buffer, Long.MAX_VALUE) != -1) {
+ hashingSink.write(buffer, buffer.size());
+ blackhole.clear();
+ }
+
+ pipe.source().close();
+ return hashingSink.hash();
+ }
+ });
+
+ assertEquals(expectedHash, sinkHash.get());
+ assertEquals(expectedHash, sourceHash.get());
+ }
+
+ @Test public void sinkTimeout() throws Exception {
+ TestUtil.INSTANCE.assumeNotWindows();
+
+ Pipe pipe = new Pipe(3);
+ pipe.sink().timeout().timeout(1000, TimeUnit.MILLISECONDS);
+ pipe.sink().write(new Buffer().writeUtf8("abc"), 3L);
+ double start = now();
+ try {
+ pipe.sink().write(new Buffer().writeUtf8("def"), 3L);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("timeout", expected.getMessage());
+ }
+ assertElapsed(1000.0, start);
+
+ Buffer readBuffer = new Buffer();
+ assertEquals(3L, pipe.source().read(readBuffer, 6L));
+ assertEquals("abc", readBuffer.readUtf8());
+ }
+
+ @Test public void sourceTimeout() throws Exception {
+ TestUtil.INSTANCE.assumeNotWindows();
+
+ Pipe pipe = new Pipe(3L);
+ pipe.source().timeout().timeout(1000, TimeUnit.MILLISECONDS);
+ double start = now();
+ Buffer readBuffer = new Buffer();
+ try {
+ pipe.source().read(readBuffer, 6L);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("timeout", expected.getMessage());
+ }
+ assertElapsed(1000.0, start);
+ assertEquals(0, readBuffer.size());
+ }
+
+ /**
+ * The writer is writing 12 bytes as fast as it can to a 3 byte buffer. The reader alternates
+ * sleeping 1000 ms, then reading 3 bytes. That should make for an approximate timeline like
+ * this:
+ *
+ * 0: writer writes 'abc', blocks 0: reader sleeps until 1000
+ * 1000: reader reads 'abc', sleeps until 2000
+ * 1000: writer writes 'def', blocks
+ * 2000: reader reads 'def', sleeps until 3000
+ * 2000: writer writes 'ghi', blocks
+ * 3000: reader reads 'ghi', sleeps until 4000
+ * 3000: writer writes 'jkl', returns
+ * 4000: reader reads 'jkl', returns
+ *
+ * Because the writer is writing to a buffer, it finishes before the reader does.
+ */
+ @Test public void sinkBlocksOnSlowReader() throws Exception {
+ final Pipe pipe = new Pipe(3L);
+ executorService.execute(new Runnable() {
+ @Override public void run() {
+ try {
+ Buffer buffer = new Buffer();
+ Thread.sleep(1000L);
+ assertEquals(3, pipe.source().read(buffer, Long.MAX_VALUE));
+ assertEquals("abc", buffer.readUtf8());
+ Thread.sleep(1000L);
+ assertEquals(3, pipe.source().read(buffer, Long.MAX_VALUE));
+ assertEquals("def", buffer.readUtf8());
+ Thread.sleep(1000L);
+ assertEquals(3, pipe.source().read(buffer, Long.MAX_VALUE));
+ assertEquals("ghi", buffer.readUtf8());
+ Thread.sleep(1000L);
+ assertEquals(3, pipe.source().read(buffer, Long.MAX_VALUE));
+ assertEquals("jkl", buffer.readUtf8());
+ } catch (IOException | InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ });
+
+ double start = now();
+ pipe.sink().write(new Buffer().writeUtf8("abcdefghijkl"), 12);
+ assertElapsed(3000.0, start);
+ }
+
+ @Test public void sinkWriteFailsByClosedReader() throws Exception {
+ final Pipe pipe = new Pipe(3L);
+ executorService.schedule(new Runnable() {
+ @Override public void run() {
+ try {
+ pipe.source().close();
+ } catch (IOException e) {
+ throw new AssertionError();
+ }
+ }
+ }, 1000, TimeUnit.MILLISECONDS);
+
+ double start = now();
+ try {
+ pipe.sink().write(new Buffer().writeUtf8("abcdef"), 6);
+ fail();
+ } catch (IOException expected) {
+ assertEquals("source is closed", expected.getMessage());
+ assertElapsed(1000.0, start);
+ }
+ }
+
+ @Test public void sinkFlushDoesntWaitForReader() throws Exception {
+ Pipe pipe = new Pipe(100L);
+ pipe.sink().write(new Buffer().writeUtf8("abc"), 3);
+ pipe.sink().flush();
+
+ BufferedSource bufferedSource = Okio.buffer(pipe.source());
+ assertEquals("abc", bufferedSource.readUtf8(3));
+ }
+
+ @Test public void sinkFlushFailsIfReaderIsClosedBeforeAllDataIsRead() throws Exception {
+ Pipe pipe = new Pipe(100L);
+ pipe.sink().write(new Buffer().writeUtf8("abc"), 3);
+ pipe.source().close();
+ try {
+ pipe.sink().flush();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("source is closed", expected.getMessage());
+ }
+ }
+
+ @Test public void sinkCloseFailsIfReaderIsClosedBeforeAllDataIsRead() throws Exception {
+ Pipe pipe = new Pipe(100L);
+ pipe.sink().write(new Buffer().writeUtf8("abc"), 3);
+ pipe.source().close();
+ try {
+ pipe.sink().close();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("source is closed", expected.getMessage());
+ }
+ }
+
+ @Test public void sinkClose() throws Exception {
+ Pipe pipe = new Pipe(100L);
+ pipe.sink().close();
+ try {
+ pipe.sink().write(new Buffer().writeUtf8("abc"), 3);
+ fail();
+ } catch (IllegalStateException expected) {
+ assertEquals("closed", expected.getMessage());
+ }
+ try {
+ pipe.sink().flush();
+ fail();
+ } catch (IllegalStateException expected) {
+ assertEquals("closed", expected.getMessage());
+ }
+ }
+
+ @Test public void sinkMultipleClose() throws Exception {
+ Pipe pipe = new Pipe(100L);
+ pipe.sink().close();
+ pipe.sink().close();
+ }
+
+ @Test public void sinkCloseDoesntWaitForSourceRead() throws Exception {
+ Pipe pipe = new Pipe(100L);
+ pipe.sink().write(new Buffer().writeUtf8("abc"), 3);
+ pipe.sink().close();
+
+ BufferedSource bufferedSource = Okio.buffer(pipe.source());
+ assertEquals("abc", bufferedSource.readUtf8());
+ assertTrue(bufferedSource.exhausted());
+ }
+
+ @Test public void sourceClose() throws Exception {
+ Pipe pipe = new Pipe(100L);
+ pipe.source().close();
+ try {
+ pipe.source().read(new Buffer(), 3);
+ fail();
+ } catch (IllegalStateException expected) {
+ assertEquals("closed", expected.getMessage());
+ }
+ }
+
+ @Test public void sourceMultipleClose() throws Exception {
+ Pipe pipe = new Pipe(100L);
+ pipe.source().close();
+ pipe.source().close();
+ }
+
+ @Test public void sourceReadUnblockedByClosedSink() throws Exception {
+ final Pipe pipe = new Pipe(3L);
+ executorService.schedule(new Runnable() {
+ @Override public void run() {
+ try {
+ pipe.sink().close();
+ } catch (IOException e) {
+ throw new AssertionError();
+ }
+ }
+ }, 1000, TimeUnit.MILLISECONDS);
+
+ double start = now();
+ Buffer readBuffer = new Buffer();
+ assertEquals(-1, pipe.source().read(readBuffer, Long.MAX_VALUE));
+ assertEquals(0, readBuffer.size());
+ assertElapsed(1000.0, start);
+ }
+
+ /**
+ * The writer has 12 bytes to write. It alternates sleeping 1000 ms, then writing 3 bytes. The
+ * reader is reading as fast as it can. That should make for an approximate timeline like this:
+ *
+ * 0: writer sleeps until 1000
+ * 0: reader blocks
+ * 1000: writer writes 'abc', sleeps until 2000
+ * 1000: reader reads 'abc'
+ * 2000: writer writes 'def', sleeps until 3000
+ * 2000: reader reads 'def'
+ * 3000: writer writes 'ghi', sleeps until 4000
+ * 3000: reader reads 'ghi'
+ * 4000: writer writes 'jkl', returns
+ * 4000: reader reads 'jkl', returns
+ */
+ @Test public void sourceBlocksOnSlowWriter() throws Exception {
+ final Pipe pipe = new Pipe(100L);
+ executorService.execute(new Runnable() {
+ @Override public void run() {
+ try {
+ Thread.sleep(1000L);
+ pipe.sink().write(new Buffer().writeUtf8("abc"), 3);
+ Thread.sleep(1000L);
+ pipe.sink().write(new Buffer().writeUtf8("def"), 3);
+ Thread.sleep(1000L);
+ pipe.sink().write(new Buffer().writeUtf8("ghi"), 3);
+ Thread.sleep(1000L);
+ pipe.sink().write(new Buffer().writeUtf8("jkl"), 3);
+ } catch (IOException | InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ });
+
+ double start = now();
+ Buffer readBuffer = new Buffer();
+
+ assertEquals(3, pipe.source().read(readBuffer, Long.MAX_VALUE));
+ assertEquals("abc", readBuffer.readUtf8());
+ assertElapsed(1000.0, start);
+
+ assertEquals(3, pipe.source().read(readBuffer, Long.MAX_VALUE));
+ assertEquals("def", readBuffer.readUtf8());
+ assertElapsed(2000.0, start);
+
+ assertEquals(3, pipe.source().read(readBuffer, Long.MAX_VALUE));
+ assertEquals("ghi", readBuffer.readUtf8());
+ assertElapsed(3000.0, start);
+
+ assertEquals(3, pipe.source().read(readBuffer, Long.MAX_VALUE));
+ assertEquals("jkl", readBuffer.readUtf8());
+ assertElapsed(4000.0, start);
+ }
+
+ /** Returns the nanotime in milliseconds as a double for measuring timeouts. */
+ private double now() {
+ return System.nanoTime() / 1000000.0d;
+ }
+
+ /**
+ * Fails the test unless the time from start until now is duration, accepting differences in
+ * -50..+450 milliseconds.
+ */
+ private void assertElapsed(double duration, double start) {
+ assertEquals(duration, now() - start - 200d, 250.0);
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/ReadUtf8LineTest.java b/okio/src/jvmTest/java/okio/ReadUtf8LineTest.java
new file mode 100644
index 00000000..9cc177f1
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/ReadUtf8LineTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(Parameterized.class)
+public final class ReadUtf8LineTest {
+ private interface Factory {
+ BufferedSource create(Buffer data);
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static List<Object[]> parameters() {
+ return Arrays.asList(
+ new Object[] { new Factory() {
+ @Override public BufferedSource create(Buffer data) {
+ return data;
+ }
+
+ @Override public String toString() {
+ return "Buffer";
+ }
+ }},
+ new Object[] { new Factory() {
+ @Override public BufferedSource create(Buffer data) {
+ return new RealBufferedSource(data);
+ }
+
+ @Override public String toString() {
+ return "RealBufferedSource";
+ }
+ }},
+ new Object[] { new Factory() {
+ @Override public BufferedSource create(Buffer data) {
+ return new RealBufferedSource(new ForwardingSource(data) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ return super.read(sink, Math.min(1, byteCount));
+ }
+ });
+ }
+
+ @Override public String toString() {
+ return "Slow RealBufferedSource";
+ }
+ }}
+ );
+ }
+
+ @Parameterized.Parameter
+ public Factory factory;
+
+ private Buffer data;
+ private BufferedSource source;
+
+ @Before public void setUp() {
+ data = new Buffer();
+ source = factory.create(data);
+ }
+
+ @Test public void readLines() throws IOException {
+ data.writeUtf8("abc\ndef\n");
+ assertEquals("abc", source.readUtf8LineStrict());
+ assertEquals("def", source.readUtf8LineStrict());
+ try {
+ source.readUtf8LineStrict();
+ fail();
+ } catch (EOFException expected) {
+ assertEquals("\\n not found: limit=0 content=…", expected.getMessage());
+ }
+ }
+
+ @Test public void readUtf8LineStrictWithLimits() throws IOException {
+ int[] lens = {1, SEGMENT_SIZE - 2, SEGMENT_SIZE - 1, SEGMENT_SIZE, SEGMENT_SIZE * 10};
+ for (int len : lens) {
+ data.writeUtf8(repeat("a", len)).writeUtf8("\n");
+ assertEquals(len, source.readUtf8LineStrict(len).length());
+ source.readUtf8();
+
+ data.writeUtf8(repeat("a", len)).writeUtf8("\n").writeUtf8(repeat("a", len));
+ assertEquals(len, source.readUtf8LineStrict(len).length());
+ source.readUtf8();
+
+ data.writeUtf8(repeat("a", len)).writeUtf8("\r\n");
+ assertEquals(len, source.readUtf8LineStrict(len).length());
+ source.readUtf8();
+
+ data.writeUtf8(repeat("a", len)).writeUtf8("\r\n").writeUtf8(repeat("a", len));
+ assertEquals(len, source.readUtf8LineStrict(len).length());
+ source.readUtf8();
+ }
+ }
+
+ @Test public void readUtf8LineStrictNoBytesConsumedOnFailure() throws IOException {
+ data.writeUtf8("abc\n");
+ try {
+ source.readUtf8LineStrict(2);
+ fail();
+ } catch (EOFException expected) {
+ assertTrue(expected.getMessage().startsWith("\\n not found: limit=2 content=61626"));
+ }
+ assertEquals("abc", source.readUtf8LineStrict(3));
+ }
+
+ @Test public void readUtf8LineStrictEmptyString() throws IOException {
+ data.writeUtf8("\r\nabc");
+ assertEquals("", source.readUtf8LineStrict(0));
+ assertEquals("abc", source.readUtf8());
+ }
+
+ @Test public void readUtf8LineStrictNonPositive() throws IOException {
+ data.writeUtf8("\r\n");
+ try {
+ source.readUtf8LineStrict(-1);
+ fail("Expected failure: limit must be greater than 0");
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void eofExceptionProvidesLimitedContent() throws IOException {
+ data.writeUtf8("aaaaaaaabbbbbbbbccccccccdddddddde");
+ try {
+ source.readUtf8LineStrict();
+ fail();
+ } catch (EOFException expected) {
+ assertEquals("\\n not found: limit=33 content=616161616161616162626262626262626363636363636363"
+ + "6464646464646464…", expected.getMessage());
+ }
+ }
+
+ @Test public void newlineAtEnd() throws IOException {
+ data.writeUtf8("abc\n");
+ assertEquals("abc", source.readUtf8LineStrict(3));
+ assertTrue(source.exhausted());
+
+ data.writeUtf8("abc\r\n");
+ assertEquals("abc", source.readUtf8LineStrict(3));
+ assertTrue(source.exhausted());
+
+ data.writeUtf8("abc\r");
+ try {
+ source.readUtf8LineStrict(3);
+ fail();
+ } catch (EOFException expected) {
+ assertEquals("\\n not found: limit=3 content=6162630d…", expected.getMessage());
+ }
+ source.readUtf8();
+
+ data.writeUtf8("abc");
+ try {
+ source.readUtf8LineStrict(3);
+ fail();
+ } catch (EOFException expected) {
+ assertEquals("\\n not found: limit=3 content=616263…", expected.getMessage());
+ }
+ }
+
+ @Test public void emptyLines() throws IOException {
+ data.writeUtf8("\n\n\n");
+ assertEquals("", source.readUtf8LineStrict());
+ assertEquals("", source.readUtf8LineStrict());
+ assertEquals("", source.readUtf8LineStrict());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void crDroppedPrecedingLf() throws IOException {
+ data.writeUtf8("abc\r\ndef\r\nghi\rjkl\r\n");
+ assertEquals("abc", source.readUtf8LineStrict());
+ assertEquals("def", source.readUtf8LineStrict());
+ assertEquals("ghi\rjkl", source.readUtf8LineStrict());
+ }
+
+ @Test public void bufferedReaderCompatible() throws IOException {
+ data.writeUtf8("abc\ndef");
+ assertEquals("abc", source.readUtf8Line());
+ assertEquals("def", source.readUtf8Line());
+ assertEquals(null, source.readUtf8Line());
+ }
+
+ @Test public void bufferedReaderCompatibleWithTrailingNewline() throws IOException {
+ data.writeUtf8("abc\ndef\n");
+ assertEquals("abc", source.readUtf8Line());
+ assertEquals("def", source.readUtf8Line());
+ assertEquals(null, source.readUtf8Line());
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/SocketTimeoutTest.java b/okio/src/jvmTest/java/okio/SocketTimeoutTest.java
new file mode 100644
index 00000000..6a6aadcd
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/SocketTimeoutTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class SocketTimeoutTest {
+
+ // The size of the socket buffers to use. Less than half the data transferred during tests to
+ // ensure send and receive buffers are flooded and any necessary blocking behavior takes place.
+ private static final int SOCKET_BUFFER_SIZE = 256 * 1024;
+ private static final int ONE_MB = 1024 * 1024;
+
+ @Test public void readWithoutTimeout() throws Exception {
+ Socket socket = socket(ONE_MB, 0);
+ BufferedSource source = Okio.buffer(Okio.source(socket));
+ source.timeout().timeout(5000, TimeUnit.MILLISECONDS);
+ source.require(ONE_MB);
+ socket.close();
+ }
+
+ @Test public void readWithTimeout() throws Exception {
+ Socket socket = socket(0, 0);
+ BufferedSource source = Okio.buffer(Okio.source(socket));
+ source.timeout().timeout(250, TimeUnit.MILLISECONDS);
+ try {
+ source.require(ONE_MB);
+ fail();
+ } catch (SocketTimeoutException expected) {
+ }
+ socket.close();
+ }
+
+ @Test public void writeWithoutTimeout() throws Exception {
+ Socket socket = socket(0, ONE_MB);
+ Sink sink = Okio.buffer(Okio.sink(socket));
+ sink.timeout().timeout(500, TimeUnit.MILLISECONDS);
+ byte[] data = new byte[ONE_MB];
+ sink.write(new Buffer().write(data), data.length);
+ sink.flush();
+ socket.close();
+ }
+
+ @Test public void writeWithTimeout() throws Exception {
+ Socket socket = socket(0, 0);
+ Sink sink = Okio.sink(socket);
+ sink.timeout().timeout(500, TimeUnit.MILLISECONDS);
+ byte[] data = new byte[ONE_MB];
+ long start = System.nanoTime();
+ try {
+ sink.write(new Buffer().write(data), data.length);
+ sink.flush();
+ fail();
+ } catch (SocketTimeoutException expected) {
+ }
+ long elapsed = System.nanoTime() - start;
+ socket.close();
+
+ assertTrue("elapsed: " + elapsed, TimeUnit.NANOSECONDS.toMillis(elapsed) >= 500);
+ assertTrue("elapsed: " + elapsed, TimeUnit.NANOSECONDS.toMillis(elapsed) <= 750);
+ }
+
+ /**
+ * Returns a socket that can read {@code readableByteCount} incoming bytes and
+ * will accept {@code writableByteCount} written bytes. The socket will idle
+ * for 5 seconds when the required data has been read and written.
+ */
+ static Socket socket(final int readableByteCount, final int writableByteCount) throws IOException {
+ final ServerSocket serverSocket = new ServerSocket(0);
+ serverSocket.setReuseAddress(true);
+ serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+
+ Thread peer = new Thread("peer") {
+ @Override public void run() {
+ Socket socket = null;
+ try {
+ socket = serverSocket.accept();
+ socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
+ writeFully(socket.getOutputStream(), readableByteCount);
+ readFully(socket.getInputStream(), writableByteCount);
+ Thread.sleep(5000); // Sleep 5 seconds so the peer can close the connection.
+ } catch (Exception ignored) {
+ } finally {
+ try {
+ if (socket != null) socket.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ };
+ peer.start();
+
+ Socket socket = new Socket(serverSocket.getInetAddress(), serverSocket.getLocalPort());
+ socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+ socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
+ return socket;
+ }
+
+ private static void writeFully(OutputStream out, int byteCount) throws IOException {
+ out.write(new byte[byteCount]);
+ out.flush();
+ }
+
+ private static byte[] readFully(InputStream in, int byteCount) throws IOException {
+ int count = 0;
+ byte[] result = new byte[byteCount];
+ while (count < byteCount) {
+ int read = in.read(result, count, result.length - count);
+ if (read == -1) throw new EOFException();
+ count += read;
+ }
+ return result;
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/Utf8Test.java b/okio/src/jvmTest/java/okio/Utf8Test.java
new file mode 100644
index 00000000..63e0b7d4
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/Utf8Test.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.EOFException;
+import kotlin.text.Charsets;
+import org.junit.Test;
+
+import static kotlin.text.StringsKt.repeat;
+import static okio.TestUtil.REPLACEMENT_CODE_POINT;
+import static okio.TestUtil.SEGMENT_SIZE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class Utf8Test {
+ @Test public void oneByteCharacters() throws Exception {
+ assertEncoded("00", 0x00); // Smallest 1-byte character.
+ assertEncoded("20", ' ');
+ assertEncoded("7e", '~');
+ assertEncoded("7f", 0x7f); // Largest 1-byte character.
+ }
+
+ @Test public void twoByteCharacters() throws Exception {
+ assertEncoded("c280", 0x0080); // Smallest 2-byte character.
+ assertEncoded("c3bf", 0x00ff);
+ assertEncoded("c480", 0x0100);
+ assertEncoded("dfbf", 0x07ff); // Largest 2-byte character.
+ }
+
+ @Test public void threeByteCharacters() throws Exception {
+ assertEncoded("e0a080", 0x0800); // Smallest 3-byte character.
+ assertEncoded("e0bfbf", 0x0fff);
+ assertEncoded("e18080", 0x1000);
+ assertEncoded("e1bfbf", 0x1fff);
+ assertEncoded("ed8080", 0xd000);
+ assertEncoded("ed9fbf", 0xd7ff); // Largest character lower than the min surrogate.
+ assertEncoded("ee8080", 0xe000); // Smallest character greater than the max surrogate.
+ assertEncoded("eebfbf", 0xefff);
+ assertEncoded("ef8080", 0xf000);
+ assertEncoded("efbfbf", 0xffff); // Largest 3-byte character.
+ }
+
+ @Test public void fourByteCharacters() throws Exception {
+ assertEncoded("f0908080", 0x010000); // Smallest surrogate pair.
+ assertEncoded("f48fbfbf", 0x10ffff); // Largest code point expressible by UTF-16.
+ }
+
+ @Test public void danglingHighSurrogate() throws Exception {
+ assertStringEncoded("3f", "\ud800"); // "?"
+ }
+
+ @Test public void lowSurrogateWithoutHighSurrogate() throws Exception {
+ assertStringEncoded("3f", "\udc00"); // "?"
+ }
+
+ @Test public void highSurrogateFollowedByNonSurrogate() throws Exception {
+ assertStringEncoded("3f61", "\ud800\u0061"); // "?a": Following character is too low.
+ assertStringEncoded("3fee8080", "\ud800\ue000"); // "?\ue000": Following character is too high.
+ }
+
+ @Test public void doubleLowSurrogate() throws Exception {
+ assertStringEncoded("3f3f", "\udc00\udc00"); // "??"
+ }
+
+ @Test public void doubleHighSurrogate() throws Exception {
+ assertStringEncoded("3f3f", "\ud800\ud800"); // "??"
+ }
+
+ @Test public void highSurrogateLowSurrogate() throws Exception {
+ assertStringEncoded("3f3f", "\udc00\ud800"); // "??"
+ }
+
+ @Test public void multipleSegmentString() throws Exception {
+ String a = repeat("a", SEGMENT_SIZE + SEGMENT_SIZE + 1);
+ Buffer encoded = new Buffer().writeUtf8(a);
+ Buffer expected = new Buffer().write(a.getBytes(Charsets.UTF_8));
+ assertEquals(expected, encoded);
+ }
+
+ @Test public void stringSpansSegments() throws Exception {
+ Buffer buffer = new Buffer();
+ String a = repeat("a", SEGMENT_SIZE - 1);
+ String b = "bb";
+ String c = repeat("c", SEGMENT_SIZE - 1);
+ buffer.writeUtf8(a);
+ buffer.writeUtf8(b);
+ buffer.writeUtf8(c);
+ assertEquals(a + b + c, buffer.readUtf8());
+ }
+
+ @Test public void readEmptyBufferThrowsEofException() throws Exception {
+ Buffer buffer = new Buffer();
+ try {
+ buffer.readUtf8CodePoint();
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void readLeadingContinuationByteReturnsReplacementCharacter() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeByte(0xbf);
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertTrue(buffer.exhausted());
+ }
+
+ @Test public void readMissingContinuationBytesThrowsEofException() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeByte(0xdf);
+ try {
+ buffer.readUtf8CodePoint();
+ fail();
+ } catch (EOFException expected) {
+ }
+ assertFalse(buffer.exhausted()); // Prefix byte wasn't consumed.
+ }
+
+ @Test public void readTooLargeCodepointReturnsReplacementCharacter() throws Exception {
+ // 5-byte and 6-byte code points are not supported.
+ Buffer buffer = new Buffer();
+ buffer.write(ByteString.decodeHex("f888808080"));
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertTrue(buffer.exhausted());
+ }
+
+ @Test public void readNonContinuationBytesReturnsReplacementCharacter() throws Exception {
+ // Use a non-continuation byte where a continuation byte is expected.
+ Buffer buffer = new Buffer();
+ buffer.write(ByteString.decodeHex("df20"));
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertEquals(0x20, buffer.readUtf8CodePoint()); // Non-continuation character not consumed.
+ assertTrue(buffer.exhausted());
+ }
+
+ @Test public void readCodePointBeyondUnicodeMaximum() throws Exception {
+ // A 4-byte encoding with data above the U+10ffff Unicode maximum.
+ Buffer buffer = new Buffer();
+ buffer.write(ByteString.decodeHex("f4908080"));
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertTrue(buffer.exhausted());
+ }
+
+ @Test public void readSurrogateCodePoint() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.write(ByteString.decodeHex("eda080"));
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertTrue(buffer.exhausted());
+ buffer.write(ByteString.decodeHex("edbfbf"));
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertTrue(buffer.exhausted());
+ }
+
+ @Test public void readOverlongCodePoint() throws Exception {
+ // Use 2 bytes to encode data that only needs 1 byte.
+ Buffer buffer = new Buffer();
+ buffer.write(ByteString.decodeHex("c080"));
+ assertEquals(REPLACEMENT_CODE_POINT, buffer.readUtf8CodePoint());
+ assertTrue(buffer.exhausted());
+ }
+
+ @Test public void writeSurrogateCodePoint() throws Exception {
+ assertStringEncoded("ed9fbf", "\ud7ff"); // Below lowest surrogate is okay.
+ assertStringEncoded("3f", "\ud800"); // Lowest surrogate gets '?'.
+ assertStringEncoded("3f", "\udfff"); // Highest surrogate gets '?'.
+ assertStringEncoded("ee8080", "\ue000"); // Above highest surrogate is okay.
+ }
+
+ @Test public void writeCodePointBeyondUnicodeMaximum() throws Exception {
+ Buffer buffer = new Buffer();
+ try {
+ buffer.writeUtf8CodePoint(0x110000);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ assertEquals("Unexpected code point: 0x110000", expected.getMessage());
+ }
+ }
+
+ @Test public void size() throws Exception {
+ assertEquals(0, Utf8.size(""));
+ assertEquals(3, Utf8.size("abc"));
+ assertEquals(16, Utf8.size("təˈranəˌsôr"));
+ }
+
+ @Test public void sizeWithBounds() throws Exception {
+ assertEquals(0, Utf8.size("", 0, 0));
+ assertEquals(0, Utf8.size("abc", 0, 0));
+ assertEquals(1, Utf8.size("abc", 1, 2));
+ assertEquals(2, Utf8.size("abc", 0, 2));
+ assertEquals(3, Utf8.size("abc", 0, 3));
+ assertEquals(16, Utf8.size("təˈranəˌsôr", 0, 11));
+ assertEquals(5, Utf8.size("təˈranəˌsôr", 3, 7));
+ }
+
+ @Test public void sizeBoundsCheck() throws Exception {
+ try {
+ Utf8.size(null, 0, 0);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ try {
+ Utf8.size("abc", -1, 2);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ try {
+ Utf8.size("abc", 2, 1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ try {
+ Utf8.size("abc", 1, 4);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ private void assertEncoded(String hex, int... codePoints) throws Exception {
+ assertCodePointEncoded(hex, codePoints);
+ assertCodePointDecoded(hex, codePoints);
+ assertStringEncoded(hex, new String(codePoints, 0, codePoints.length));
+ }
+
+ private void assertCodePointEncoded(String hex, int... codePoints) throws Exception {
+ Buffer buffer = new Buffer();
+ for (int codePoint : codePoints) {
+ buffer.writeUtf8CodePoint(codePoint);
+ }
+ assertEquals(buffer.readByteString(), ByteString.decodeHex(hex));
+ }
+
+ private void assertCodePointDecoded(String hex, int... codePoints) throws Exception {
+ Buffer buffer = new Buffer().write(ByteString.decodeHex(hex));
+ for (int codePoint : codePoints) {
+ assertEquals(codePoint, buffer.readUtf8CodePoint());
+ }
+ assertTrue(buffer.exhausted());
+ }
+
+ private void assertStringEncoded(String hex, String string) throws Exception {
+ ByteString expectedUtf8 = ByteString.decodeHex(hex);
+
+ // Confirm our expectations are consistent with the platform.
+ ByteString platformUtf8 = ByteString.of(string.getBytes("UTF-8"));
+ assertEquals(expectedUtf8, platformUtf8);
+
+ // Confirm our implementation matches those expectations.
+ ByteString actualUtf8 = new Buffer().writeUtf8(string).readByteString();
+ assertEquals(expectedUtf8, actualUtf8);
+
+ // Confirm we are consistent when writing one code point at a time.
+ Buffer bufferUtf8 = new Buffer();
+ for (int i = 0; i < string.length(); ) {
+ int c = string.codePointAt(i);
+ bufferUtf8.writeUtf8CodePoint(c);
+ i += Character.charCount(c);
+ }
+ assertEquals(expectedUtf8, bufferUtf8.readByteString());
+
+ // Confirm we are consistent when measuring lengths.
+ assertEquals(expectedUtf8.size(), Utf8.size(string));
+ assertEquals(expectedUtf8.size(), Utf8.size(string, 0, string.length()));
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/WaitUntilNotifiedTest.java b/okio/src/jvmTest/java/okio/WaitUntilNotifiedTest.java
new file mode 100644
index 00000000..e4405289
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/WaitUntilNotifiedTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio;
+
+import java.io.InterruptedIOException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class WaitUntilNotifiedTest {
+ final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(0);
+
+ @After public void tearDown() {
+ executorService.shutdown();
+ }
+
+ @Test public synchronized void notified() throws InterruptedIOException {
+ Timeout timeout = new Timeout();
+ timeout.timeout(5000, TimeUnit.MILLISECONDS);
+
+ double start = now();
+ executorService.schedule(new Runnable() {
+ @Override public void run() {
+ synchronized (WaitUntilNotifiedTest.this) {
+ WaitUntilNotifiedTest.this.notify();
+ }
+ }
+ }, 1000, TimeUnit.MILLISECONDS);
+
+ timeout.waitUntilNotified(this);
+ assertElapsed(1000.0, start);
+ }
+
+ @Test public synchronized void timeout() {
+ TestUtil.INSTANCE.assumeNotWindows();
+
+ Timeout timeout = new Timeout();
+ timeout.timeout(1000, TimeUnit.MILLISECONDS);
+ double start = now();
+ try {
+ timeout.waitUntilNotified(this);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("timeout", expected.getMessage());
+ }
+ assertElapsed(1000.0, start);
+ }
+
+ @Test public synchronized void deadline() {
+ TestUtil.INSTANCE.assumeNotWindows();
+
+ Timeout timeout = new Timeout();
+ timeout.deadline(1000, TimeUnit.MILLISECONDS);
+ double start = now();
+ try {
+ timeout.waitUntilNotified(this);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("timeout", expected.getMessage());
+ }
+ assertElapsed(1000.0, start);
+ }
+
+ @Test public synchronized void deadlineBeforeTimeout() {
+ TestUtil.INSTANCE.assumeNotWindows();
+
+ Timeout timeout = new Timeout();
+ timeout.timeout(5000, TimeUnit.MILLISECONDS);
+ timeout.deadline(1000, TimeUnit.MILLISECONDS);
+ double start = now();
+ try {
+ timeout.waitUntilNotified(this);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("timeout", expected.getMessage());
+ }
+ assertElapsed(1000.0, start);
+ }
+
+ @Test public synchronized void timeoutBeforeDeadline() {
+ TestUtil.INSTANCE.assumeNotWindows();
+
+ Timeout timeout = new Timeout();
+ timeout.timeout(1000, TimeUnit.MILLISECONDS);
+ timeout.deadline(5000, TimeUnit.MILLISECONDS);
+ double start = now();
+ try {
+ timeout.waitUntilNotified(this);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("timeout", expected.getMessage());
+ }
+ assertElapsed(1000.0, start);
+ }
+
+ @Test public synchronized void deadlineAlreadyReached() {
+ TestUtil.INSTANCE.assumeNotWindows();
+
+ Timeout timeout = new Timeout();
+ timeout.deadlineNanoTime(System.nanoTime());
+ double start = now();
+ try {
+ timeout.waitUntilNotified(this);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("timeout", expected.getMessage());
+ }
+ assertElapsed(0.0, start);
+ }
+
+ @Test public synchronized void threadInterrupted() {
+ TestUtil.INSTANCE.assumeNotWindows();
+
+ Timeout timeout = new Timeout();
+ double start = now();
+ Thread.currentThread().interrupt();
+ try {
+ timeout.waitUntilNotified(this);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("interrupted", expected.getMessage());
+ assertTrue(Thread.interrupted());
+ }
+ assertElapsed(0.0, start);
+ }
+
+ @Test public synchronized void threadInterruptedOnThrowIfReached() throws Exception {
+ TestUtil.INSTANCE.assumeNotWindows();
+
+ Timeout timeout = new Timeout();
+ Thread.currentThread().interrupt();
+ try {
+ timeout.throwIfReached();
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("interrupted", expected.getMessage());
+ assertTrue(Thread.interrupted());
+ }
+ }
+
+ /** Returns the nanotime in milliseconds as a double for measuring timeouts. */
+ private double now() {
+ return System.nanoTime() / 1000000.0d;
+ }
+
+ /**
+ * Fails the test unless the time from start until now is duration, accepting differences in
+ * -50..+450 milliseconds.
+ */
+ private void assertElapsed(double duration, double start) {
+ assertEquals(duration, now() - start - 200d, 250.0);
+ }
+}
diff --git a/okio/src/jvmTest/java/okio/internal/HmacTest.kt b/okio/src/jvmTest/java/okio/internal/HmacTest.kt
new file mode 100644
index 00000000..0c5a7f43
--- /dev/null
+++ b/okio/src/jvmTest/java/okio/internal/HmacTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.internal
+
+import okio.ByteString
+import org.junit.Assert.assertArrayEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+import kotlin.random.Random
+
+/**
+ * Check the [Hmac] implementation against the reference [Mac] JVM implementation.
+ */
+@RunWith(Parameterized::class)
+class HmacTest(val parameters: Parameters) {
+
+ companion object {
+ @get:Parameterized.Parameters(name = "{0}")
+ @get:JvmStatic
+ val parameters: List<Parameters>
+ get() {
+ val algorithms = enumValues<Parameters.Algorithm>()
+ val keySizes = listOf(8, 32, 48, 64, 128, 256)
+ val dataSizes = listOf(0, 32, 64, 128, 256, 512)
+ return algorithms.flatMap { algorithm ->
+ keySizes.flatMap { keySize ->
+ dataSizes.map { dataSize ->
+ Parameters(
+ algorithm,
+ keySize,
+ dataSize
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private val keySize
+ get() = parameters.keySize
+ private val dataSize
+ get() = parameters.dataSize
+ private val algorithm
+ get() = parameters.algorithmName
+
+ private val random = Random(682741861446)
+
+ private val key = random.nextBytes(keySize)
+ private val bytes = random.nextBytes(dataSize)
+ private val mac = parameters.createMac(key)
+
+ private val expected = hmac(algorithm, key, bytes)
+
+ @Test
+ fun hmac() {
+ mac.update(bytes)
+ val hmacValue = mac.digest()
+
+ assertArrayEquals(expected, hmacValue)
+ }
+
+ @Test
+ fun hmacBytes() {
+ for (byte in bytes) {
+ mac.update(byteArrayOf(byte))
+ }
+ val hmacValue = mac.digest()
+
+ assertArrayEquals(expected, hmacValue)
+ }
+
+ data class Parameters(
+ val algorithm: Algorithm,
+ val keySize: Int,
+ val dataSize: Int
+ ) {
+ val algorithmName
+ get() = algorithm.algorithmName
+
+ internal fun createMac(key: ByteArray) =
+ algorithm.HmacFactory(ByteString(key))
+
+ enum class Algorithm(
+ val algorithmName: String,
+ internal val HmacFactory: (key: ByteString) -> Hmac
+ ) {
+ SHA_1("HmacSha1", Hmac.Companion::sha1),
+ SHA_256("HmacSha256", Hmac.Companion::sha256),
+ SHA_512("HmacSha512", Hmac.Companion::sha512),
+ }
+ }
+}
+
+private fun hmac(algorithm: String, key: ByteArray, bytes: ByteArray) =
+ Mac.getInstance(algorithm).apply { init(SecretKeySpec(key, algorithm)) }.doFinal(bytes)
diff --git a/okio/src/jvmTest/kotlin/okio/BufferCursorKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/BufferCursorKotlinTest.kt
new file mode 100644
index 00000000..ddbc4c53
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/BufferCursorKotlinTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.Buffer.UnsafeCursor
+import okio.TestUtil.deepCopy
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameter
+import org.junit.runners.Parameterized.Parameters
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotSame
+import kotlin.test.assertSame
+import kotlin.test.assertTrue
+
+@RunWith(Parameterized::class)
+class BufferCursorKotlinTest {
+ companion object {
+ @Parameters(name = "{0}")
+ @JvmStatic
+ fun parameters(): List<Array<out Any?>> {
+ return BufferFactory.values().map { arrayOf(it) }
+ }
+ }
+
+ @Parameter lateinit var bufferFactory: BufferFactory
+
+ @Test fun acquireReadOnlyDoesNotCopySharedDataArray() {
+ val buffer = deepCopy(bufferFactory.newBuffer())
+ assumeTrue(buffer.size > 0L)
+
+ val shared = buffer.clone()
+ assertTrue(buffer.head!!.shared)
+
+ buffer.readUnsafe().use { cursor ->
+ cursor.seek(0)
+ assertSame(cursor.data, shared.head!!.data)
+ }
+ }
+
+ @Test fun acquireReadWriteDoesNotCopyUnsharedDataArray() {
+ val buffer = deepCopy(bufferFactory.newBuffer())
+ assumeTrue(buffer.size > 0L)
+ assertFalse(buffer.head!!.shared)
+
+ val originalData = buffer.head!!.data
+
+ buffer.readAndWriteUnsafe().use { cursor ->
+ cursor.seek(0)
+ assertSame(cursor.data, originalData)
+ }
+ }
+
+ @Test fun acquireReadWriteCopiesSharedDataArray() {
+ val buffer = deepCopy(bufferFactory.newBuffer())
+ assumeTrue(buffer.size > 0L)
+
+ val shared = buffer.clone()
+ assertTrue(buffer.head!!.shared)
+
+ buffer.readAndWriteUnsafe().use { cursor ->
+ cursor.seek(0)
+ assertNotSame(cursor.data, shared.head!!.data)
+ }
+ }
+
+ @Test fun writeSharedSegments() {
+ val buffer = bufferFactory.newBuffer()
+
+ // Make a deep copy. This buffer's segments are not shared.
+ val deepCopy = deepCopy(buffer)
+ assertTrue(deepCopy.head == null || !deepCopy.head!!.shared)
+
+ // Make a shallow copy. Both buffers' segments are shared as a side effect.
+ val shallowCopy = buffer.clone()
+ assertTrue(shallowCopy.head == null || shallowCopy.head!!.shared)
+ assertTrue(buffer.head == null || buffer.head!!.shared)
+
+ val expected = Buffer()
+ expected.writeUtf8("x".repeat(buffer.size.toInt()))
+
+ buffer.readAndWriteUnsafe().use { cursor ->
+ while (cursor.next() != -1) {
+ cursor.data!!.fill('x'.toByte(), cursor.start, cursor.end)
+ }
+ }
+
+ // The buffer was fully changed.
+ assertEquals(expected, buffer)
+
+ // The buffer we're shared with is unchanged.
+ assertEquals(deepCopy, shallowCopy)
+ }
+
+ /** As an optimization it's okay to use the same cursor on multiple buffers. */
+ @Test fun cursorReuse() {
+ val cursor = UnsafeCursor()
+
+ val buffer1 = bufferFactory.newBuffer()
+ buffer1.readUnsafe(cursor)
+ assertSame(buffer1, cursor.buffer)
+ assertFalse(cursor.readWrite)
+ cursor.close()
+ assertSame(null, cursor.buffer)
+
+ val buffer2 = bufferFactory.newBuffer()
+ buffer2.readAndWriteUnsafe(cursor)
+ assertSame(buffer2, cursor.buffer)
+ assertTrue(cursor.readWrite)
+ cursor.close()
+ assertSame(null, cursor.buffer)
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/BufferFactory.kt b/okio/src/jvmTest/kotlin/okio/BufferFactory.kt
new file mode 100644
index 00000000..e1533d23
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/BufferFactory.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.TestUtil.bufferWithRandomSegmentLayout
+import okio.TestUtil.bufferWithSegments
+import java.util.Random
+
+enum class BufferFactory {
+ EMPTY {
+ override fun newBuffer(): Buffer {
+ return Buffer()
+ }
+ },
+
+ SMALL_BUFFER {
+ override fun newBuffer(): Buffer {
+ return Buffer().writeUtf8("abcde")
+ }
+ },
+
+ SMALL_SEGMENTED_BUFFER {
+ @Throws(Exception::class)
+ override fun newBuffer(): Buffer {
+ return bufferWithSegments("abc", "defg", "hijkl")
+ }
+ },
+
+ LARGE_BUFFER {
+ @Throws(Exception::class)
+ override fun newBuffer(): Buffer {
+ val dice = Random(0)
+ val largeByteArray = ByteArray(512 * 1024)
+ dice.nextBytes(largeByteArray)
+
+ return Buffer().write(largeByteArray)
+ }
+ },
+
+ LARGE_BUFFER_WITH_RANDOM_LAYOUT {
+ @Throws(Exception::class)
+ override fun newBuffer(): Buffer {
+ val dice = Random(0)
+ val largeByteArray = ByteArray(512 * 1024)
+ dice.nextBytes(largeByteArray)
+
+ return bufferWithRandomSegmentLayout(dice, largeByteArray)
+ }
+ };
+
+ @Throws(Exception::class)
+ abstract fun newBuffer(): Buffer
+}
diff --git a/okio/src/jvmTest/kotlin/okio/BufferKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/BufferKotlinTest.kt
new file mode 100644
index 00000000..eda7989d
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/BufferKotlinTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import kotlin.test.assertFailsWith
+
+class BufferKotlinTest {
+ @Test fun get() {
+ val actual = Buffer().writeUtf8("abc")
+ assertThat(actual[0]).isEqualTo('a'.toByte())
+ assertThat(actual[1]).isEqualTo('b'.toByte())
+ assertThat(actual[2]).isEqualTo('c'.toByte())
+ assertFailsWith<IndexOutOfBoundsException> {
+ actual[-1]
+ }
+ assertFailsWith<IndexOutOfBoundsException> {
+ actual[3]
+ }
+ }
+
+ @Test fun copyToOutputStream() {
+ val source = Buffer()
+ source.writeUtf8("party")
+
+ val target = Buffer()
+ source.copyTo(target.outputStream())
+ assertThat(target.readUtf8()).isEqualTo("party")
+ assertThat(source.readUtf8()).isEqualTo("party")
+ }
+
+ @Test fun copyToOutputStreamWithOffset() {
+ val source = Buffer()
+ source.writeUtf8("party")
+
+ val target = Buffer()
+ source.copyTo(target.outputStream(), offset = 2)
+ assertThat(target.readUtf8()).isEqualTo("rty")
+ assertThat(source.readUtf8()).isEqualTo("party")
+ }
+
+ @Test fun copyToOutputStreamWithByteCount() {
+ val source = Buffer()
+ source.writeUtf8("party")
+
+ val target = Buffer()
+ source.copyTo(target.outputStream(), byteCount = 3)
+ assertThat(target.readUtf8()).isEqualTo("par")
+ assertThat(source.readUtf8()).isEqualTo("party")
+ }
+
+ @Test fun copyToOutputStreamWithOffsetAndByteCount() {
+ val source = Buffer()
+ source.writeUtf8("party")
+
+ val target = Buffer()
+ source.copyTo(target.outputStream(), offset = 1, byteCount = 3)
+ assertThat(target.readUtf8()).isEqualTo("art")
+ assertThat(source.readUtf8()).isEqualTo("party")
+ }
+
+ @Test fun writeToOutputStream() {
+ val source = Buffer()
+ source.writeUtf8("party")
+
+ val target = Buffer()
+ source.writeTo(target.outputStream())
+ assertThat(target.readUtf8()).isEqualTo("party")
+ assertThat(source.readUtf8()).isEqualTo("")
+ }
+
+ @Test fun writeToOutputStreamWithByteCount() {
+ val source = Buffer()
+ source.writeUtf8("party")
+
+ val target = Buffer()
+ source.writeTo(target.outputStream(), byteCount = 3)
+ assertThat(target.readUtf8()).isEqualTo("par")
+ assertThat(source.readUtf8()).isEqualTo("ty")
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/ByteStringKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/ByteStringKotlinTest.kt
new file mode 100644
index 00000000..c554251f
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/ByteStringKotlinTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.ByteString.Companion.encode
+import okio.ByteString.Companion.encodeUtf8
+import okio.ByteString.Companion.readByteString
+import okio.ByteString.Companion.toByteString
+import java.io.ByteArrayInputStream
+import java.nio.ByteBuffer
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class ByteStringKotlinTest {
+ @Test fun arrayToByteString() {
+ val actual = byteArrayOf(1, 2, 3, 4).toByteString()
+ val expected = ByteString.of(1, 2, 3, 4)
+ assertEquals(actual, expected)
+ }
+
+ @Test fun arraySubsetToByteString() {
+ val actual = byteArrayOf(1, 2, 3, 4).toByteString(1, 2)
+ val expected = ByteString.of(2, 3)
+ assertEquals(actual, expected)
+ }
+
+ @Test fun byteBufferToByteString() {
+ val actual = ByteBuffer.wrap(byteArrayOf(1, 2, 3, 4)).toByteString()
+ val expected = ByteString.of(1, 2, 3, 4)
+ assertEquals(actual, expected)
+ }
+
+ @Test fun stringEncodeByteStringDefaultCharset() {
+ val actual = "a\uD83C\uDF69c".encode()
+ val expected = "a\uD83C\uDF69c".encodeUtf8()
+ assertEquals(actual, expected)
+ }
+
+ @Test fun streamReadByteString() {
+ val stream = ByteArrayInputStream(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8))
+ val actual = stream.readByteString(4)
+ val expected = ByteString.of(1, 2, 3, 4)
+ assertEquals(actual, expected)
+ }
+
+ @Test fun substring() {
+ val byteString = "abcdef".encodeUtf8()
+ assertEquals(byteString.substring(), "abcdef".encodeUtf8())
+ assertEquals(byteString.substring(endIndex = 3), "abc".encodeUtf8())
+ assertEquals(byteString.substring(beginIndex = 3), "def".encodeUtf8())
+ assertEquals(byteString.substring(beginIndex = 1, endIndex = 5), "bcde".encodeUtf8())
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/CipherAlgorithm.kt b/okio/src/jvmTest/kotlin/okio/CipherAlgorithm.kt
new file mode 100644
index 00000000..f9f42b03
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/CipherAlgorithm.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import kotlin.random.Random
+
+data class CipherAlgorithm(
+ val transformation: String,
+ val padding: Boolean,
+ val keyLength: Int,
+ val ivLength: Int? = null
+) {
+ fun createCipherFactory(random: Random): CipherFactory {
+ val key = random.nextBytes(keyLength)
+ val secretKeySpec = SecretKeySpec(key, transformation.substringBefore('/'))
+ return if (ivLength == null) {
+ CipherFactory(transformation) { mode ->
+ init(mode, secretKeySpec)
+ }
+ } else {
+ val iv = random.nextBytes(ivLength)
+ val ivParameterSpec = IvParameterSpec(iv)
+ CipherFactory(transformation) { mode ->
+ init(mode, secretKeySpec, ivParameterSpec)
+ }
+ }
+ }
+
+ override fun toString() = transformation
+
+ companion object {
+ val BLOCK_CIPHER_ALGORITHMS
+ get() = listOf(
+ CipherAlgorithm("AES/CBC/NoPadding", false, 16, 16),
+ CipherAlgorithm("AES/CBC/PKCS5Padding", true, 16, 16),
+ CipherAlgorithm("AES/ECB/NoPadding", false, 16),
+ CipherAlgorithm("AES/ECB/PKCS5Padding", true, 16),
+ CipherAlgorithm("DES/CBC/NoPadding", false, 8, 8),
+ CipherAlgorithm("DES/CBC/PKCS5Padding", true, 8, 8),
+ CipherAlgorithm("DES/ECB/NoPadding", false, 8),
+ CipherAlgorithm("DES/ECB/PKCS5Padding", true, 8),
+ CipherAlgorithm("DESede/CBC/NoPadding", false, 24, 8),
+ CipherAlgorithm("DESede/CBC/PKCS5Padding", true, 24, 8),
+ CipherAlgorithm("DESede/ECB/NoPadding", false, 24),
+ CipherAlgorithm("DESede/ECB/PKCS5Padding", true, 24)
+ )
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/CipherFactory.kt b/okio/src/jvmTest/kotlin/okio/CipherFactory.kt
new file mode 100644
index 00000000..7b93c652
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/CipherFactory.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import javax.crypto.Cipher
+
+class CipherFactory(
+ private val transformation: String,
+ private val init: Cipher.(mode: Int) -> Unit
+) {
+ val blockSize
+ get() = newCipher().blockSize
+
+ val encrypt: Cipher
+ get() = create(Cipher.ENCRYPT_MODE)
+
+ val decrypt: Cipher
+ get() = create(Cipher.DECRYPT_MODE)
+
+ private fun newCipher(): Cipher = Cipher.getInstance(transformation)
+
+ private fun create(mode: Int): Cipher = newCipher().apply { init(mode) }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/CipherSinkTest.kt b/okio/src/jvmTest/kotlin/okio/CipherSinkTest.kt
new file mode 100644
index 00000000..f273971d
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/CipherSinkTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2020 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import kotlin.random.Random
+
+@RunWith(Parameterized::class)
+class CipherSinkTest(private val cipherAlgorithm: CipherAlgorithm) {
+ companion object {
+ @get:Parameterized.Parameters(name = "{0}")
+ @get:JvmStatic
+ val parameters: List<CipherAlgorithm>
+ get() = CipherAlgorithm.BLOCK_CIPHER_ALGORITHMS
+ }
+
+ @Test
+ fun encrypt() {
+ val random = Random(8912860393601532863)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val data = random.nextBytes(32)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.encrypt)
+ cipherSink.buffer().use { it.write(data) }
+ val actualEncryptedData = buffer.readByteArray()
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ @Test
+ fun encryptEmpty() {
+ val random = Random(3014415396541767201)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val data = ByteArray(0)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.encrypt)
+ cipherSink.buffer().close()
+ val actualEncryptedData = buffer.readByteArray()
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ @Test
+ fun encryptLarge() {
+ val random = Random(4800508322764694019)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val data = random.nextBytes(Segment.SIZE * 16 + Segment.SIZE / 2)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.encrypt)
+ cipherSink.buffer().use { it.write(data) }
+ val actualEncryptedData = buffer.readByteArray()
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ @Test
+ fun encryptSingleByteWrite() {
+ val random = Random(4374178522096702290)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val data = random.nextBytes(32)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.encrypt)
+ cipherSink.buffer().use {
+ data.forEach {
+ byte ->
+ it.writeByte(byte.toInt())
+ }
+ }
+ val actualEncryptedData = buffer.readByteArray()
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ /** Only relevant for algorithms which handle padding. */
+ @Test
+ fun encryptPaddingRequired() {
+ val random = Random(7515202505362968404)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val blockSize = cipherFactory.blockSize
+ val dataSize = blockSize * 4 + if (cipherAlgorithm.padding) blockSize / 2 else 0
+ val data = random.nextBytes(dataSize)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.encrypt)
+ cipherSink.buffer().use { it.write(data) }
+ val actualEncryptedData = buffer.readByteArray()
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ @Test
+ fun decrypt() {
+ val random = Random(488375923060579687)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val expectedData = random.nextBytes(32)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.decrypt)
+ cipherSink.buffer().use { it.write(encryptedData) }
+ val actualData = buffer.readByteArray()
+
+ assertArrayEquals(expectedData, actualData)
+ }
+
+ @Test
+ fun decryptEmpty() {
+ val random = Random(-9063010151894844496)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val expectedData = ByteArray(0)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.decrypt)
+ cipherSink.buffer().use { it.write(encryptedData) }
+ val actualData = buffer.readByteArray()
+
+ assertArrayEquals(expectedData, actualData)
+ }
+
+ @Test
+ fun decryptLarge() {
+ val random = Random(993064087526004362)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val expectedData = random.nextBytes(Segment.SIZE * 16 + Segment.SIZE / 2)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.decrypt)
+ cipherSink.buffer().use { it.write(encryptedData) }
+ val actualData = buffer.readByteArray()
+
+ assertArrayEquals(expectedData, actualData)
+ }
+
+ @Test
+ fun decryptSingleByteWrite() {
+ val random = Random(2621474675920878975)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val expectedData = random.nextBytes(32)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.decrypt)
+ cipherSink.buffer().use {
+ encryptedData.forEach { byte ->
+ it.writeByte(byte.toInt())
+ }
+ }
+ val actualData = buffer.readByteArray()
+
+ assertArrayEquals(expectedData, actualData)
+ }
+
+ /** Only relevant for algorithms which handle padding. */
+ @Test
+ fun decryptPaddingRequired() {
+ val random = Random(7689061926945836562)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val blockSize = cipherFactory.blockSize
+ val dataSize = blockSize * 4 + if (cipherAlgorithm.padding) blockSize / 2 else 0
+ val expectedData = random.nextBytes(dataSize)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer()
+ val cipherSink = buffer.cipherSink(cipherFactory.decrypt)
+ cipherSink.buffer().use { it.write(encryptedData) }
+ val actualData = buffer.readByteArray()
+
+ assertArrayEquals(expectedData, actualData)
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/CipherSourceTest.kt b/okio/src/jvmTest/kotlin/okio/CipherSourceTest.kt
new file mode 100644
index 00000000..f97258e2
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/CipherSourceTest.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2020 Square, Inc. and others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import kotlin.random.Random
+
+@RunWith(Parameterized::class)
+class CipherSourceTest(private val cipherAlgorithm: CipherAlgorithm) {
+ companion object {
+ @get:Parameterized.Parameters(name = "{0}")
+ @get:JvmStatic
+ val parameters: List<CipherAlgorithm>
+ get() = CipherAlgorithm.BLOCK_CIPHER_ALGORITHMS
+ }
+
+ @Test
+ fun encrypt() {
+ val random = Random(787679144228763091)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val data = random.nextBytes(32)
+
+ val buffer = Buffer().apply { write(data) }
+ val cipherSource = buffer.cipherSource(cipherFactory.encrypt)
+ val actualEncryptedData = cipherSource.buffer().use { it.readByteArray() }
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ @Test
+ fun encryptEmpty() {
+ val random = Random(1057830944394705953)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val data = ByteArray(0)
+
+ val buffer = Buffer()
+ val cipherSource = buffer.cipherSource(cipherFactory.encrypt)
+ val actualEncryptedData = cipherSource.buffer().use { it.readByteArray() }
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ @Test
+ fun encryptLarge() {
+ val random = Random(8185922876836480815)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val data = random.nextBytes(Segment.SIZE * 16 + Segment.SIZE / 2)
+
+ val buffer = Buffer().apply { write(data) }
+ val cipherSource = buffer.cipherSource(cipherFactory.encrypt)
+ val actualEncryptedData = cipherSource.buffer().use { it.readByteArray() }
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ @Test
+ fun encryptSingleByteSource() {
+ val random = Random(6085265142433950622)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val data = random.nextBytes(32)
+
+ val buffer = Buffer().apply { write(data) }
+ val cipherSource = buffer.emitSingleBytes().cipherSource(cipherFactory.encrypt)
+ val actualEncryptedData = cipherSource.buffer().use { it.readByteArray() }
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ /** Only relevant for algorithms which handle padding. */
+ @Test
+ fun encryptPaddingRequired() {
+ val random = Random(4190481737015278225)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val blockSize = cipherFactory.blockSize
+ val dataSize = blockSize * 4 + if (cipherAlgorithm.padding) blockSize / 2 else 0
+ val data = random.nextBytes(dataSize)
+
+ val buffer = Buffer().apply { write(data) }
+ val cipherSource = buffer.cipherSource(cipherFactory.encrypt)
+ val actualEncryptedData = cipherSource.buffer().use { it.readByteArray() }
+
+ val expectedEncryptedData = cipherFactory.encrypt.doFinal(data)
+ assertArrayEquals(expectedEncryptedData, actualEncryptedData)
+ }
+
+ @Test
+ fun decrypt() {
+ val random = Random(8067587635762239433)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val expectedData = random.nextBytes(32)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer().apply { write(encryptedData) }
+ val cipherSource = buffer.cipherSource(cipherFactory.decrypt)
+ val actualData = cipherSource.buffer().use { it.readByteArray() }
+
+ assertArrayEquals(expectedData, actualData)
+ }
+
+ @Test
+ fun decryptEmpty() {
+ val random = Random(8722996896871347396)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val expectedData = ByteArray(0)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer().apply { write(encryptedData) }
+ val cipherSource = buffer.cipherSource(cipherFactory.decrypt)
+ val actualData = cipherSource.buffer().use { it.readByteArray() }
+
+ assertArrayEquals(expectedData, actualData)
+ }
+
+ @Test
+ fun decryptLarge() {
+ val random = Random(4007116131070653181)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val expectedData = random.nextBytes(Segment.SIZE * 16 + Segment.SIZE / 2)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer().apply { write(encryptedData) }
+ val cipherSource = buffer.cipherSource(cipherFactory.decrypt)
+ val actualData = cipherSource.buffer().use { it.readByteArray() }
+
+ assertArrayEquals(expectedData, actualData)
+ }
+
+ @Test
+ fun decryptSingleByteSource() {
+ val random = Random(1555017938547616655)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val expectedData = random.nextBytes(32)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer().apply { write(encryptedData) }
+ val cipherSource = buffer.emitSingleBytes().cipherSource(cipherFactory.decrypt)
+ val actualData = cipherSource.buffer().use {
+ it.readByteArray()
+ }
+
+ assertArrayEquals(expectedData, actualData)
+ }
+
+ /** Only relevant for algorithms which handle padding. */
+ @Test
+ fun decryptPaddingRequired() {
+ val random = Random(5717921427007554469)
+ val cipherFactory = cipherAlgorithm.createCipherFactory(random)
+ val blockSize = cipherFactory.blockSize
+ val dataSize = blockSize * 4 + if (cipherAlgorithm.padding) blockSize / 2 else 0
+ val expectedData = random.nextBytes(dataSize)
+ val encryptedData = cipherFactory.encrypt.doFinal(expectedData)
+
+ val buffer = Buffer().apply { write(encryptedData) }
+ val cipherSource = buffer.cipherSource(cipherFactory.decrypt)
+ val actualData = cipherSource.buffer().use { it.readByteArray() }
+
+ assertArrayEquals(expectedData, actualData)
+ }
+
+ private fun Source.emitSingleBytes(): Source =
+ SingleByteSource(this)
+
+ private class SingleByteSource(source: Source) : ForwardingSource(source) {
+ override fun read(sink: Buffer, byteCount: Long): Long =
+ delegate.read(sink, 1L)
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/DeflateKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/DeflateKotlinTest.kt
new file mode 100644
index 00000000..7a1744ad
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/DeflateKotlinTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.ByteString.Companion.decodeHex
+import org.junit.Test
+import java.util.zip.Deflater
+import java.util.zip.Inflater
+import kotlin.test.assertEquals
+
+class DeflateKotlinTest {
+ @Test fun deflate() {
+ val data = Buffer()
+ val deflater = (data as Sink).deflate()
+ deflater.buffer().writeUtf8("Hi!").close()
+ assertEquals("789cf3c854040001ce00d3", data.readByteString().hex())
+ }
+
+ @Test fun deflateWithDeflater() {
+ val data = Buffer()
+ val deflater = (data as Sink).deflate(Deflater(0, true))
+ deflater.buffer().writeUtf8("Hi!").close()
+ assertEquals("010300fcff486921", data.readByteString().hex())
+ }
+
+ @Test fun inflate() {
+ val buffer = Buffer().write("789cf3c854040001ce00d3".decodeHex())
+ val inflated = (buffer as Source).inflate()
+ assertEquals("Hi!", inflated.buffer().readUtf8())
+ }
+
+ @Test fun inflateWithInflater() {
+ val buffer = Buffer().write("010300fcff486921".decodeHex())
+ val inflated = (buffer as Source).inflate(Inflater(true))
+ assertEquals("Hi!", inflated.buffer().readUtf8())
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutKotlinTest.kt
new file mode 100644
index 00000000..4db02c50
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/ForwardingTimeoutKotlinTest.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+
+class ForwardingTimeoutKotlinTest {
+ @Test fun getAndSetDelegate() {
+ val timeout1 = Timeout()
+ val timeout2 = Timeout()
+
+ val forwardingTimeout = ForwardingTimeout(timeout1)
+ forwardingTimeout.timeout(5, TimeUnit.SECONDS)
+ assertThat(timeout1.timeoutNanos()).isNotEqualTo(0L)
+ assertThat(timeout2.timeoutNanos()).isEqualTo(0L)
+ forwardingTimeout.clearTimeout()
+ assertThat(timeout1.timeoutNanos()).isEqualTo(0L)
+ assertThat(timeout2.timeoutNanos()).isEqualTo(0L)
+ assertThat(forwardingTimeout.delegate).isEqualTo(timeout1)
+
+ forwardingTimeout.delegate = timeout2
+ forwardingTimeout.timeout(5, TimeUnit.SECONDS)
+ assertThat(timeout1.timeoutNanos()).isEqualTo(0L)
+ assertThat(timeout2.timeoutNanos()).isNotEqualTo(0L)
+ forwardingTimeout.clearTimeout()
+ assertThat(timeout1.timeoutNanos()).isEqualTo(0L)
+ assertThat(timeout2.timeoutNanos()).isEqualTo(0L)
+ assertThat(forwardingTimeout.delegate).isEqualTo(timeout2)
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/GzipKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/GzipKotlinTest.kt
new file mode 100644
index 00000000..6bbe9b51
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/GzipKotlinTest.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.ByteString.Companion.decodeHex
+import org.junit.Test
+import kotlin.test.assertEquals
+
+class GzipKotlinTest {
+ @Test fun sink() {
+ val data = Buffer()
+ val gzip = (data as Sink).gzip()
+ gzip.buffer().writeUtf8("Hi!").close()
+ assertEquals("1f8b0800000000000000f3c8540400dac59e7903000000", data.readByteString().hex())
+ }
+
+ @Test fun source() {
+ val buffer = Buffer().write("1f8b0800000000000000f3c8540400dac59e7903000000".decodeHex())
+ val gzip = (buffer as Source).gzip()
+ assertEquals("Hi!", gzip.buffer().readUtf8())
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/OkioKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/OkioKotlinTest.kt
new file mode 100644
index 00000000..58c6bcad
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/OkioKotlinTest.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.net.Socket
+import java.nio.file.StandardOpenOption
+import java.nio.file.StandardOpenOption.APPEND
+
+class OkioKotlinTest {
+ @get:Rule val temp = TemporaryFolder()
+
+ @Test fun outputStreamSink() {
+ val baos = ByteArrayOutputStream()
+ val sink = baos.sink()
+ sink.write(Buffer().writeUtf8("a"), 1L)
+ assertThat(baos.toByteArray()).isEqualTo(byteArrayOf(0x61))
+ }
+
+ @Test fun inputStreamSource() {
+ val bais = ByteArrayInputStream(byteArrayOf(0x61))
+ val source = bais.source()
+ val buffer = Buffer()
+ source.read(buffer, 1)
+ assertThat(buffer.readUtf8()).isEqualTo("a")
+ }
+
+ @Test fun fileSink() {
+ val file = temp.newFile()
+ val sink = file.sink()
+ sink.write(Buffer().writeUtf8("a"), 1L)
+ assertThat(file.readText()).isEqualTo("a")
+ }
+
+ @Test fun fileAppendingSink() {
+ val file = temp.newFile()
+ file.writeText("a")
+ val sink = file.sink(append = true)
+ sink.write(Buffer().writeUtf8("b"), 1L)
+ sink.close()
+ assertThat(file.readText()).isEqualTo("ab")
+ }
+
+ @Test fun fileSource() {
+ val file = temp.newFile()
+ file.writeText("a")
+ val source = file.source()
+ val buffer = Buffer()
+ source.read(buffer, 1L)
+ assertThat(buffer.readUtf8()).isEqualTo("a")
+ }
+
+ @Test fun pathSink() {
+ val file = temp.newFile()
+ val sink = file.toPath().sink()
+ sink.write(Buffer().writeUtf8("a"), 1L)
+ assertThat(file.readText()).isEqualTo("a")
+ }
+
+ @Test fun pathSinkWithOptions() {
+ val file = temp.newFile()
+ file.writeText("a")
+ val sink = file.toPath().sink(APPEND)
+ sink.write(Buffer().writeUtf8("b"), 1L)
+ assertThat(file.readText()).isEqualTo("ab")
+ }
+
+ @Test fun pathSource() {
+ val file = temp.newFile()
+ file.writeText("a")
+ val source = file.toPath().source()
+ val buffer = Buffer()
+ source.read(buffer, 1L)
+ assertThat(buffer.readUtf8()).isEqualTo("a")
+ }
+
+ @Ignore("Not sure how to test this")
+ @Test fun pathSourceWithOptions() {
+ val folder = temp.newFolder()
+ val file = File(folder, "new.txt")
+ file.toPath().source(StandardOpenOption.CREATE_NEW)
+ // This still throws NoSuchFileException...
+ }
+
+ @Test fun socketSink() {
+ val baos = ByteArrayOutputStream()
+ val socket = object : Socket() {
+ override fun getOutputStream() = baos
+ }
+ val sink = socket.sink()
+ sink.write(Buffer().writeUtf8("a"), 1L)
+ assertThat(baos.toByteArray()).isEqualTo(byteArrayOf(0x61))
+ }
+
+ @Test fun socketSource() {
+ val bais = ByteArrayInputStream(byteArrayOf(0x61))
+ val socket = object : Socket() {
+ override fun getInputStream() = bais
+ }
+ val source = socket.source()
+ val buffer = Buffer()
+ source.read(buffer, 1L)
+ assertThat(buffer.readUtf8()).isEqualTo("a")
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/PipeKotlinTest.kt b/okio/src/jvmTest/kotlin/okio/PipeKotlinTest.kt
new file mode 100644
index 00000000..3a41e742
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/PipeKotlinTest.kt
@@ -0,0 +1,883 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import java.io.IOException
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertFailsWith
+import org.junit.rules.Timeout as JUnitTimeout
+
+class PipeKotlinTest {
+ @JvmField @Rule val timeout = JUnitTimeout(5, TimeUnit.SECONDS)
+
+ private val executorService = Executors.newScheduledThreadPool(1)
+
+ @After @Throws(Exception::class)
+ fun tearDown() {
+ executorService.shutdown()
+ }
+
+ @Test fun pipe() {
+ val pipe = Pipe(6)
+ pipe.sink.write(Buffer().writeUtf8("abc"), 3L)
+
+ val readBuffer = Buffer()
+ assertEquals(3L, pipe.source.read(readBuffer, 6L))
+ assertEquals("abc", readBuffer.readUtf8())
+
+ pipe.sink.close()
+ assertEquals(-1L, pipe.source.read(readBuffer, 6L))
+
+ pipe.source.close()
+ }
+
+ @Test fun fold() {
+ val pipe = Pipe(128)
+
+ val pipeSink = pipe.sink.buffer()
+ pipeSink.writeUtf8("hello")
+ pipeSink.emit()
+
+ val pipeSource = pipe.source.buffer()
+ assertEquals("hello", pipeSource.readUtf8(5))
+
+ val foldedSinkBuffer = Buffer()
+ var foldedSinkClosed = false
+ val foldedSink = object : ForwardingSink(foldedSinkBuffer) {
+ override fun close() {
+ foldedSinkClosed = true
+ super.close()
+ }
+ }
+ pipe.fold(foldedSink)
+
+ pipeSink.writeUtf8("world")
+ pipeSink.emit()
+ assertEquals("world", foldedSinkBuffer.readUtf8(5))
+
+ assertFailsWith<IllegalStateException> {
+ pipeSource.readUtf8()
+ }
+
+ pipeSink.close()
+ assertTrue(foldedSinkClosed)
+ }
+
+ @Test fun foldWritesPipeContentsToSink() {
+ val pipe = Pipe(128)
+
+ val pipeSink = pipe.sink.buffer()
+ pipeSink.writeUtf8("hello")
+ pipeSink.emit()
+
+ val foldSink = Buffer()
+ pipe.fold(foldSink)
+
+ assertEquals("hello", foldSink.readUtf8(5))
+ }
+
+ @Test fun foldUnblocksBlockedWrite() {
+ val pipe = Pipe(4)
+ val foldSink = Buffer()
+
+ val latch = CountDownLatch(1)
+ executorService.schedule(
+ {
+ pipe.fold(foldSink)
+ latch.countDown()
+ },
+ 500, TimeUnit.MILLISECONDS
+ )
+
+ val sink = pipe.sink.buffer()
+ sink.writeUtf8("abcdefgh") // Blocks writing 8 bytes to a 4 byte pipe.
+ sink.close()
+
+ latch.await()
+ assertEquals("abcdefgh", foldSink.readUtf8())
+ }
+
+ @Test fun accessSourceAfterFold() {
+ val pipe = Pipe(100L)
+ pipe.fold(Buffer())
+ assertFailsWith<IllegalStateException> {
+ pipe.source.read(Buffer(), 1L)
+ }
+ }
+
+ @Test fun honorsPipeSinkTimeoutOnWritingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutWritingSink()
+
+ underlying.timeout.timeout(biggerTimeoutNanos, TimeUnit.NANOSECONDS)
+ pipe.sink.timeout().timeout(smallerTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerTimeoutNanos) {
+ pipe.sink.write(Buffer().writeUtf8("abc"), 3)
+ }
+ assertEquals(biggerTimeoutNanos, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsUnderlyingTimeoutOnWritingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutWritingSink()
+
+ underlying.timeout.timeout(smallerTimeoutNanos, TimeUnit.NANOSECONDS)
+ pipe.sink.timeout().timeout(biggerTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerTimeoutNanos) {
+ pipe.sink.write(Buffer().writeUtf8("abc"), 3)
+ }
+ assertEquals(smallerTimeoutNanos, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsPipeSinkTimeoutOnFlushingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutFlushingSink()
+
+ underlying.timeout.timeout(biggerTimeoutNanos, TimeUnit.NANOSECONDS)
+ pipe.sink.timeout().timeout(smallerTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerTimeoutNanos) {
+ pipe.sink.flush()
+ }
+ assertEquals(biggerTimeoutNanos, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsUnderlyingTimeoutOnFlushingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutFlushingSink()
+
+ underlying.timeout.timeout(smallerTimeoutNanos, TimeUnit.NANOSECONDS)
+ pipe.sink.timeout().timeout(biggerTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerTimeoutNanos) {
+ pipe.sink.flush()
+ }
+ assertEquals(smallerTimeoutNanos, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsPipeSinkTimeoutOnClosingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutClosingSink()
+
+ underlying.timeout.timeout(biggerTimeoutNanos, TimeUnit.NANOSECONDS)
+ pipe.sink.timeout().timeout(smallerTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerTimeoutNanos) {
+ pipe.sink.close()
+ }
+ assertEquals(biggerTimeoutNanos, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsUnderlyingTimeoutOnClosingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutClosingSink()
+
+ underlying.timeout.timeout(smallerTimeoutNanos, TimeUnit.NANOSECONDS)
+ pipe.sink.timeout().timeout(biggerTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerTimeoutNanos) {
+ pipe.sink.close()
+ }
+ assertEquals(smallerTimeoutNanos, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsPipeSinkTimeoutOnWritingWhenUnderlyingSinkTimeoutIsZero() {
+ val pipeSinkTimeoutNanos = smallerTimeoutNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutWritingSink()
+
+ pipe.sink.timeout().timeout(pipeSinkTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(pipeSinkTimeoutNanos) {
+ pipe.sink.write(Buffer().writeUtf8("abc"), 3)
+ }
+ assertEquals(0L, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsUnderlyingSinkTimeoutOnWritingWhenPipeSinkTimeoutIsZero() {
+ val underlyingSinkTimeoutNanos = smallerTimeoutNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutWritingSink()
+
+ underlying.timeout().timeout(underlyingSinkTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(underlyingSinkTimeoutNanos) {
+ pipe.sink.write(Buffer().writeUtf8("abc"), 3)
+ }
+ assertEquals(underlyingSinkTimeoutNanos, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsPipeSinkTimeoutOnFlushingWhenUnderlyingSinkTimeoutIsZero() {
+ val pipeSinkTimeoutNanos = smallerTimeoutNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutFlushingSink()
+
+ pipe.sink.timeout().timeout(pipeSinkTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(pipeSinkTimeoutNanos) {
+ pipe.sink.flush()
+ }
+ assertEquals(0L, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsUnderlyingSinkTimeoutOnFlushingWhenPipeSinkTimeoutIsZero() {
+ val underlyingSinkTimeoutNanos = smallerTimeoutNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutFlushingSink()
+
+ underlying.timeout().timeout(underlyingSinkTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(underlyingSinkTimeoutNanos) {
+ pipe.sink.flush()
+ }
+ assertEquals(underlyingSinkTimeoutNanos, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsPipeSinkTimeoutOnClosingWhenUnderlyingSinkTimeoutIsZero() {
+ val pipeSinkTimeoutNanos = smallerTimeoutNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutClosingSink()
+
+ pipe.sink.timeout().timeout(pipeSinkTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(pipeSinkTimeoutNanos) {
+ pipe.sink.close()
+ }
+ assertEquals(0L, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsUnderlyingSinkTimeoutOnClosingWhenPipeSinkTimeoutIsZero() {
+ val underlyingSinkTimeoutNanos = smallerTimeoutNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutClosingSink()
+
+ underlying.timeout().timeout(underlyingSinkTimeoutNanos, TimeUnit.NANOSECONDS)
+
+ pipe.fold(underlying)
+
+ assertDuration(underlyingSinkTimeoutNanos) {
+ pipe.sink.close()
+ }
+ assertEquals(underlyingSinkTimeoutNanos, underlying.timeout().timeoutNanos())
+ }
+
+ @Test fun honorsPipeSinkDeadlineOnWritingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutWritingSink()
+
+ val underlyingOriginalDeadline = System.nanoTime() + biggerDeadlineNanos
+ underlying.timeout.deadlineNanoTime(underlyingOriginalDeadline)
+ pipe.sink.timeout().deadlineNanoTime(System.nanoTime() + smallerDeadlineNanos)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerDeadlineNanos) {
+ pipe.sink.write(Buffer().writeUtf8("abc"), 3)
+ }
+ assertEquals(underlyingOriginalDeadline, underlying.timeout().deadlineNanoTime())
+ }
+
+ @Test fun honorsPipeSinkDeadlineOnWritingWhenUnderlyingSinkHasNoDeadline() {
+ val deadlineNanos = smallerDeadlineNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutWritingSink()
+
+ underlying.timeout.clearDeadline()
+ pipe.sink.timeout().deadlineNanoTime(System.nanoTime() + deadlineNanos)
+
+ pipe.fold(underlying)
+
+ assertDuration(deadlineNanos) {
+ pipe.sink.write(Buffer().writeUtf8("abc"), 3)
+ }
+ assertFalse(underlying.timeout().hasDeadline())
+ }
+
+ @Test fun honorsUnderlyingSinkDeadlineOnWritingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutWritingSink()
+
+ val underlyingOriginalDeadline = System.nanoTime() + smallerDeadlineNanos
+ underlying.timeout.deadlineNanoTime(underlyingOriginalDeadline)
+ pipe.sink.timeout().deadlineNanoTime(System.nanoTime() + biggerDeadlineNanos)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerDeadlineNanos) {
+ pipe.sink.write(Buffer().writeUtf8("abc"), 3)
+ }
+ assertEquals(underlyingOriginalDeadline, underlying.timeout().deadlineNanoTime())
+ }
+
+ @Test fun honorsUnderlyingSinkDeadlineOnWritingWhenPipeSinkHasNoDeadline() {
+ val deadlineNanos = smallerDeadlineNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutWritingSink()
+
+ val underlyingOriginalDeadline = System.nanoTime() + deadlineNanos
+ underlying.timeout().deadlineNanoTime(underlyingOriginalDeadline)
+ pipe.sink.timeout().clearDeadline()
+
+ pipe.fold(underlying)
+
+ assertDuration(deadlineNanos) {
+ pipe.sink.write(Buffer().writeUtf8("abc"), 3)
+ }
+ assertEquals(underlyingOriginalDeadline, underlying.timeout().deadlineNanoTime())
+ }
+
+ @Test fun honorsPipeSinkDeadlineOnFlushingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutFlushingSink()
+
+ val underlyingOriginalDeadline = System.nanoTime() + biggerDeadlineNanos
+ underlying.timeout.deadlineNanoTime(underlyingOriginalDeadline)
+ pipe.sink.timeout().deadlineNanoTime(System.nanoTime() + smallerDeadlineNanos)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerDeadlineNanos) {
+ pipe.sink.flush()
+ }
+ assertEquals(underlyingOriginalDeadline, underlying.timeout().deadlineNanoTime())
+ }
+
+ @Test fun honorsPipeSinkDeadlineOnFlushingWhenUnderlyingSinkHasNoDeadline() {
+ val deadlineNanos = smallerDeadlineNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutFlushingSink()
+
+ underlying.timeout.clearDeadline()
+ pipe.sink.timeout().deadlineNanoTime(System.nanoTime() + deadlineNanos)
+
+ pipe.fold(underlying)
+
+ assertDuration(deadlineNanos) {
+ pipe.sink.flush()
+ }
+ assertFalse(underlying.timeout().hasDeadline())
+ }
+
+ @Test fun honorsUnderlyingSinkDeadlineOnFlushingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutFlushingSink()
+
+ val underlyingOriginalDeadline = System.nanoTime() + smallerDeadlineNanos
+ underlying.timeout.deadlineNanoTime(underlyingOriginalDeadline)
+ pipe.sink.timeout().deadlineNanoTime(System.nanoTime() + biggerDeadlineNanos)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerDeadlineNanos) {
+ pipe.sink.flush()
+ }
+ assertEquals(underlyingOriginalDeadline, underlying.timeout().deadlineNanoTime())
+ }
+
+ @Test fun honorsUnderlyingSinkDeadlineOnFlushingWhenPipeSinkHasNoDeadline() {
+ val deadlineNanos = smallerDeadlineNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutFlushingSink()
+
+ val underlyingOriginalDeadline = System.nanoTime() + deadlineNanos
+ underlying.timeout().deadlineNanoTime(underlyingOriginalDeadline)
+ pipe.sink.timeout().clearDeadline()
+
+ pipe.fold(underlying)
+
+ assertDuration(deadlineNanos) {
+ pipe.sink.flush()
+ }
+ assertEquals(underlyingOriginalDeadline, underlying.timeout().deadlineNanoTime())
+ }
+
+ @Test fun honorsPipeSinkDeadlineOnClosingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutClosingSink()
+
+ val underlyingOriginalDeadline = System.nanoTime() + biggerDeadlineNanos
+ underlying.timeout.deadlineNanoTime(underlyingOriginalDeadline)
+ pipe.sink.timeout().deadlineNanoTime(System.nanoTime() + smallerDeadlineNanos)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerDeadlineNanos) {
+ pipe.sink.close()
+ }
+ assertEquals(underlyingOriginalDeadline, underlying.timeout().deadlineNanoTime())
+ }
+
+ @Test fun honorsPipeSinkDeadlineOnClosingWhenUnderlyingSinkHasNoDeadline() {
+ val deadlineNanos = smallerDeadlineNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutClosingSink()
+
+ underlying.timeout.clearDeadline()
+ pipe.sink.timeout().deadlineNanoTime(System.nanoTime() + deadlineNanos)
+
+ pipe.fold(underlying)
+
+ assertDuration(deadlineNanos) {
+ pipe.sink.close()
+ }
+ assertFalse(underlying.timeout().hasDeadline())
+ }
+
+ @Test fun honorsUnderlyingSinkDeadlineOnClosingWhenItIsSmaller() {
+ val pipe = Pipe(4)
+ val underlying = TimeoutClosingSink()
+
+ val underlyingOriginalDeadline = System.nanoTime() + smallerDeadlineNanos
+ underlying.timeout.deadlineNanoTime(underlyingOriginalDeadline)
+ pipe.sink.timeout().deadlineNanoTime(System.nanoTime() + biggerDeadlineNanos)
+
+ pipe.fold(underlying)
+
+ assertDuration(smallerDeadlineNanos) {
+ pipe.sink.close()
+ }
+ assertEquals(underlyingOriginalDeadline, underlying.timeout().deadlineNanoTime())
+ }
+
+ @Test fun honorsUnderlyingSinkDeadlineOnClosingWhenPipeSinkHasNoDeadline() {
+ val deadlineNanos = smallerDeadlineNanos
+
+ val pipe = Pipe(4)
+ val underlying = TimeoutClosingSink()
+
+ val underlyingOriginalDeadline = System.nanoTime() + deadlineNanos
+ underlying.timeout().deadlineNanoTime(underlyingOriginalDeadline)
+ pipe.sink.timeout().clearDeadline()
+
+ pipe.fold(underlying)
+
+ assertDuration(deadlineNanos) {
+ pipe.sink.close()
+ }
+ assertEquals(underlyingOriginalDeadline, underlying.timeout().deadlineNanoTime())
+ }
+
+ @Test fun foldingTwiceThrows() {
+ val pipe = Pipe(128)
+ pipe.fold(Buffer())
+ assertFailsWith<IllegalStateException> {
+ pipe.fold(Buffer())
+ }
+ }
+
+ @Test fun sinkWriteThrowsIOExceptionUnblockBlockedWriter() {
+ val pipe = Pipe(4)
+
+ val foldFuture = executorService.schedule(
+ {
+ val foldFailure = assertFailsWith<IOException> {
+ pipe.fold(object : ForwardingSink(blackholeSink()) {
+ override fun write(source: Buffer, byteCount: Long) {
+ throw IOException("boom")
+ }
+ })
+ }
+ assertEquals("boom", foldFailure.message)
+ },
+ 500, TimeUnit.MILLISECONDS
+ )
+
+ val writeFailure = assertFailsWith<IOException> {
+ val pipeSink = pipe.sink.buffer()
+ pipeSink.writeUtf8("abcdefghij")
+ pipeSink.emit() // Block writing 10 bytes to a 4 byte pipe.
+ }
+ assertEquals("source is closed", writeFailure.message)
+
+ foldFuture.get() // Confirm no unexpected exceptions.
+ }
+
+ @Test fun foldHoldsNoLocksWhenForwardingWrites() {
+ val pipe = Pipe(4)
+
+ val pipeSink = pipe.sink.buffer()
+ pipeSink.writeUtf8("abcd")
+ pipeSink.emit()
+
+ pipe.fold(object : ForwardingSink(blackholeSink()) {
+ override fun write(source: Buffer, byteCount: Long) {
+ assertFalse(Thread.holdsLock(pipe.buffer))
+ }
+ })
+ }
+
+ /**
+ * Flushing the pipe wasn't causing the sink to be flushed when it was later folded. This was
+ * causing problems because the folded data was stalled.
+ */
+ @Test fun foldFlushesWhenThereIsFoldedData() {
+ val pipe = Pipe(128)
+ val pipeSink = pipe.sink.buffer()
+ pipeSink.writeUtf8("hello")
+ pipeSink.emit()
+
+ val ultimateSink = Buffer()
+ val unnecessaryWrapper = (ultimateSink as Sink).buffer()
+
+ pipe.fold(unnecessaryWrapper)
+
+ // Data should not have been flushed through the wrapper to the ultimate sink.
+ assertEquals("hello", ultimateSink.readUtf8())
+ }
+
+ @Test fun foldDoesNotFlushWhenThereIsNoFoldedData() {
+ val pipe = Pipe(128)
+
+ val ultimateSink = Buffer()
+ val unnecessaryWrapper = (ultimateSink as Sink).buffer()
+ unnecessaryWrapper.writeUtf8("hello")
+
+ pipe.fold(unnecessaryWrapper)
+
+ // Data should not have been flushed through the wrapper to the ultimate sink.
+ assertEquals("", ultimateSink.readUtf8())
+ }
+
+ @Test fun foldingClosesUnderlyingSinkWhenPipeSinkIsClose() {
+ val pipe = Pipe(128)
+
+ val pipeSink = pipe.sink.buffer()
+ pipeSink.writeUtf8("world")
+ pipeSink.close()
+
+ val foldedSinkBuffer = Buffer()
+ var foldedSinkClosed = false
+ val foldedSink = object : ForwardingSink(foldedSinkBuffer) {
+ override fun close() {
+ foldedSinkClosed = true
+ super.close()
+ }
+ }
+
+ pipe.fold(foldedSink)
+ assertEquals("world", foldedSinkBuffer.readUtf8(5))
+ assertTrue(foldedSinkClosed)
+ }
+
+ @Test fun cancelPreventsSinkWrite() {
+ val pipe = Pipe(8)
+ pipe.cancel()
+
+ val pipeSink = pipe.sink.buffer()
+ pipeSink.writeUtf8("hello world")
+
+ try {
+ pipeSink.emit()
+ fail()
+ } catch (e: IOException) {
+ assertEquals("canceled", e.message)
+ }
+ }
+
+ @Test fun cancelPreventsSinkFlush() {
+ val pipe = Pipe(8)
+ pipe.cancel()
+
+ try {
+ pipe.sink.flush()
+ fail()
+ } catch (e: IOException) {
+ assertEquals("canceled", e.message)
+ }
+ }
+
+ @Test fun sinkCloseAfterCancelDoesNotThrow() {
+ val pipe = Pipe(8)
+ pipe.cancel()
+ pipe.sink.close()
+ }
+
+ @Test fun cancelInterruptsSinkWrite() {
+ val pipe = Pipe(8)
+
+ executorService.schedule(
+ {
+ pipe.cancel()
+ },
+ smallerTimeoutNanos, TimeUnit.NANOSECONDS
+ )
+
+ val pipeSink = pipe.sink.buffer()
+ pipeSink.writeUtf8("hello world")
+
+ assertDuration(smallerTimeoutNanos) {
+ try {
+ pipeSink.emit()
+ fail()
+ } catch (e: IOException) {
+ assertEquals("canceled", e.message)
+ }
+ }
+ }
+
+ @Test fun cancelPreventsSourceRead() {
+ val pipe = Pipe(8)
+ pipe.cancel()
+
+ val pipeSource = pipe.source.buffer()
+
+ try {
+ pipeSource.require(1)
+ fail()
+ } catch (e: IOException) {
+ assertEquals("canceled", e.message)
+ }
+ }
+
+ @Test fun sourceCloseAfterCancelDoesNotThrow() {
+ val pipe = Pipe(8)
+ pipe.cancel()
+ pipe.source.close()
+ }
+
+ @Test fun cancelInterruptsSourceRead() {
+ val pipe = Pipe(8)
+
+ executorService.schedule(
+ {
+ pipe.cancel()
+ },
+ smallerTimeoutNanos, TimeUnit.NANOSECONDS
+ )
+
+ val pipeSource = pipe.source.buffer()
+
+ assertDuration(smallerTimeoutNanos) {
+ try {
+ pipeSource.require(1)
+ fail()
+ } catch (e: IOException) {
+ assertEquals("canceled", e.message)
+ }
+ }
+ }
+
+ @Test fun cancelPreventsSinkFold() {
+ val pipe = Pipe(8)
+ pipe.cancel()
+
+ var foldedSinkClosed = false
+ val foldedSink = object : ForwardingSink(Buffer()) {
+ override fun close() {
+ foldedSinkClosed = true
+ super.close()
+ }
+ }
+
+ try {
+ pipe.fold(foldedSink)
+ fail()
+ } catch (e: IOException) {
+ assertEquals("canceled", e.message)
+ }
+
+ // But the fold is still performed so close() closes everything.
+ assertFalse(foldedSinkClosed)
+ pipe.sink.close()
+ assertTrue(foldedSinkClosed)
+ }
+
+ @Test fun cancelInterruptsSinkFold() {
+ val pipe = Pipe(128)
+ val pipeSink = pipe.sink.buffer()
+ pipeSink.writeUtf8("hello")
+ pipeSink.emit()
+
+ var foldedSinkClosed = false
+ val foldedSink = object : ForwardingSink(Buffer()) {
+ override fun write(source: Buffer, byteCount: Long) {
+ assertEquals("hello", source.readUtf8(byteCount))
+
+ // Write bytes to the original pipe so the pipe write doesn't complete!
+ pipeSink.writeUtf8("more bytes")
+ pipeSink.emit()
+
+ // Cancel while the pipe is writing.
+ pipe.cancel()
+ }
+
+ override fun close() {
+ foldedSinkClosed = true
+ super.close()
+ }
+ }
+
+ try {
+ pipe.fold(foldedSink)
+ fail()
+ } catch (e: IOException) {
+ assertEquals("canceled", e.message)
+ }
+
+ // But the fold is still performed so close() closes everything.
+ assertFalse(foldedSinkClosed)
+ pipe.sink.close()
+ assertTrue(foldedSinkClosed)
+ }
+
+ private fun assertDuration(expected: Long, block: () -> Unit) {
+ val start = System.currentTimeMillis()
+ block()
+ val elapsed = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis() - start)
+
+ assertEquals(
+ expected.toDouble(), elapsed.toDouble(),
+ TimeUnit.MILLISECONDS.toNanos(200).toDouble()
+ )
+ }
+
+ /** Writes on this sink never complete. They can only time out. */
+ class TimeoutWritingSink : Sink {
+ val timeout = object : AsyncTimeout() {
+ override fun timedOut() {
+ synchronized(this@TimeoutWritingSink) {
+ (this@TimeoutWritingSink as Object).notifyAll()
+ }
+ }
+ }
+
+ override fun write(source: Buffer, byteCount: Long) {
+ timeout.enter()
+ try {
+ synchronized(this) {
+ (this as Object).wait()
+ }
+ } finally {
+ timeout.exit()
+ }
+ source.skip(byteCount)
+ }
+
+ override fun flush() = Unit
+
+ override fun close() = Unit
+
+ override fun timeout() = timeout
+ }
+
+ /** Flushes on this sink never complete. They can only time out. */
+ class TimeoutFlushingSink : Sink {
+ val timeout = object : AsyncTimeout() {
+ override fun timedOut() {
+ synchronized(this@TimeoutFlushingSink) {
+ (this@TimeoutFlushingSink as Object).notifyAll()
+ }
+ }
+ }
+
+ override fun write(source: Buffer, byteCount: Long) = source.skip(byteCount)
+
+ override fun flush() {
+ timeout.enter()
+ try {
+ synchronized(this) {
+ (this as Object).wait()
+ }
+ } finally {
+ timeout.exit()
+ }
+ }
+
+ override fun close() = Unit
+
+ override fun timeout() = timeout
+ }
+
+ /** Closes on this sink never complete. They can only time out. */
+ class TimeoutClosingSink : Sink {
+ val timeout = object : AsyncTimeout() {
+ override fun timedOut() {
+ synchronized(this@TimeoutClosingSink) {
+ (this@TimeoutClosingSink as Object).notifyAll()
+ }
+ }
+ }
+
+ override fun write(source: Buffer, byteCount: Long) = source.skip(byteCount)
+
+ override fun flush() = Unit
+
+ override fun close() {
+ timeout.enter()
+ try {
+ synchronized(this) {
+ (this as Object).wait()
+ }
+ } finally {
+ timeout.exit()
+ }
+ }
+
+ override fun timeout() = timeout
+ }
+
+ companion object {
+ val smallerTimeoutNanos = TimeUnit.MILLISECONDS.toNanos(500L)
+ val biggerTimeoutNanos = TimeUnit.MILLISECONDS.toNanos(1500L)
+
+ val smallerDeadlineNanos = TimeUnit.MILLISECONDS.toNanos(500L)
+ val biggerDeadlineNanos = TimeUnit.MILLISECONDS.toNanos(1500L)
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/SegmentSharingTest.kt b/okio/src/jvmTest/kotlin/okio/SegmentSharingTest.kt
new file mode 100644
index 00000000..73e74d99
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/SegmentSharingTest.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.ByteString.Companion.encodeUtf8
+import okio.TestUtil.assertEquivalent
+import okio.TestUtil.bufferWithSegments
+import okio.TestUtil.takeAllPoolSegments
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+/** Tests behavior optimized by sharing segments between buffers and byte strings. */
+class SegmentSharingTest {
+ @Test fun snapshotOfEmptyBuffer() {
+ val snapshot = Buffer().snapshot()
+ assertEquivalent(snapshot, ByteString.EMPTY)
+ }
+
+ @Test fun snapshotsAreEquivalent() {
+ val byteString = bufferWithSegments(xs, ys, zs).snapshot()
+ assertEquivalent(byteString, bufferWithSegments(xs, ys + zs).snapshot())
+ assertEquivalent(byteString, bufferWithSegments(xs + ys + zs).snapshot())
+ assertEquivalent(byteString, (xs + ys + zs).encodeUtf8())
+ }
+
+ @Test fun snapshotGetByte() {
+ val byteString = bufferWithSegments(xs, ys, zs).snapshot()
+ assertEquals('x', byteString[0].toChar())
+ assertEquals('x', byteString[xs.length - 1].toChar())
+ assertEquals('y', byteString[xs.length].toChar())
+ assertEquals('y', byteString[xs.length + ys.length - 1].toChar())
+ assertEquals('z', byteString[xs.length + ys.length].toChar())
+ assertEquals('z', byteString[xs.length + ys.length + zs.length - 1].toChar())
+ assertFailsWith<IndexOutOfBoundsException> {
+ byteString[-1]
+ }
+
+ assertFailsWith<IndexOutOfBoundsException> {
+ byteString[xs.length + ys.length + zs.length]
+ }
+ }
+
+ @Test fun snapshotWriteToOutputStream() {
+ val byteString = bufferWithSegments(xs, ys, zs).snapshot()
+ val out = Buffer()
+ byteString.write(out.outputStream())
+ assertEquals(xs + ys + zs, out.readUtf8())
+ }
+
+ /**
+ * Snapshots share their backing byte arrays with the source buffers. Those byte arrays must not
+ * be recycled, otherwise the new writer could corrupt the segment.
+ */
+ @Test fun snapshotSegmentsAreNotRecycled() {
+ val buffer = bufferWithSegments(xs, ys, zs)
+ val snapshot = buffer.snapshot()
+ assertEquals(xs + ys + zs, snapshot.utf8())
+
+ // Confirm that clearing the buffer doesn't release its segments.
+ val bufferHead = buffer.head
+ takeAllPoolSegments() // Make room for new segments.
+ buffer.clear()
+ assertTrue(bufferHead !in takeAllPoolSegments())
+ }
+
+ /**
+ * Clones share their backing byte arrays with the source buffers. Those byte arrays must not
+ * be recycled, otherwise the new writer could corrupt the segment.
+ */
+ @Test fun cloneSegmentsAreNotRecycled() {
+ val buffer = bufferWithSegments(xs, ys, zs)
+ val clone = buffer.clone()
+
+ // While locking the pool, confirm that clearing the buffer doesn't release its segments.
+ val bufferHead = buffer.head!!
+ takeAllPoolSegments() // Make room for new segments.
+ buffer.clear()
+ assertTrue(bufferHead !in takeAllPoolSegments())
+
+ val cloneHead = clone.head!!
+ takeAllPoolSegments() // Make room for new segments.
+ clone.clear()
+ assertTrue(cloneHead !in takeAllPoolSegments())
+ }
+
+ @Test fun snapshotJavaSerialization() {
+ val byteString = bufferWithSegments(xs, ys, zs).snapshot()
+ assertEquivalent(byteString, TestUtil.reserialize(byteString))
+ }
+
+ @Test fun clonesAreEquivalent() {
+ val bufferA = bufferWithSegments(xs, ys, zs)
+ val bufferB = bufferA.clone()
+ assertEquivalent(bufferA, bufferB)
+ assertEquivalent(bufferA, bufferWithSegments(xs + ys, zs))
+ }
+
+ /** Even though some segments are shared, clones can be mutated independently. */
+ @Test fun mutateAfterClone() {
+ val bufferA = Buffer()
+ bufferA.writeUtf8("abc")
+ val bufferB = bufferA.clone()
+ bufferA.writeUtf8("def")
+ bufferB.writeUtf8("DEF")
+ assertEquals("abcdef", bufferA.readUtf8())
+ assertEquals("abcDEF", bufferB.readUtf8())
+ }
+
+ @Test fun concatenateSegmentsCanCombine() {
+ val bufferA = Buffer().writeUtf8(ys).writeUtf8(us)
+ assertEquals(ys, bufferA.readUtf8(ys.length.toLong()))
+ val bufferB = Buffer().writeUtf8(vs).writeUtf8(ws)
+ val bufferC = bufferA.clone()
+ bufferA.write(bufferB, vs.length.toLong())
+ bufferC.writeUtf8(xs)
+
+ assertEquals(us + vs, bufferA.readUtf8())
+ assertEquals(ws, bufferB.readUtf8())
+ assertEquals(us + xs, bufferC.readUtf8())
+ }
+
+ @Test fun shareAndSplit() {
+ val bufferA = Buffer().writeUtf8("xxxx")
+ val snapshot = bufferA.snapshot() // Share the segment.
+ val bufferB = Buffer()
+ bufferB.write(bufferA, 2) // Split the shared segment in two.
+ bufferB.writeUtf8("yy") // Append to the first half of the shared segment.
+ assertEquals("xxxx", snapshot.utf8())
+ }
+
+ @Test fun appendSnapshotToEmptyBuffer() {
+ val bufferA = bufferWithSegments(xs, ys)
+ val snapshot = bufferA.snapshot()
+ val bufferB = Buffer()
+ bufferB.write(snapshot)
+ assertEquivalent(bufferB, bufferA)
+ }
+
+ @Test fun appendSnapshotToNonEmptyBuffer() {
+ val bufferA = bufferWithSegments(xs, ys)
+ val snapshot = bufferA.snapshot()
+ val bufferB = Buffer().writeUtf8(us)
+ bufferB.write(snapshot)
+ assertEquivalent(bufferB, Buffer().writeUtf8(us + xs + ys))
+ }
+
+ @Test fun copyToSegmentSharing() {
+ val bufferA = bufferWithSegments(ws, xs + "aaaa", ys, "bbbb$zs")
+ val bufferB = bufferWithSegments(us)
+ bufferA.copyTo(bufferB, (ws.length + xs.length).toLong(), (4 + ys.length + 4).toLong())
+ assertEquivalent(bufferB, Buffer().writeUtf8(us + "aaaa" + ys + "bbbb"))
+ }
+}
+
+private val us = "u".repeat(Segment.SIZE / 2 - 2)
+private val vs = "v".repeat(Segment.SIZE / 2 - 1)
+private val ws = "w".repeat(Segment.SIZE / 2)
+private val xs = "x".repeat(Segment.SIZE / 2 + 1)
+private val ys = "y".repeat(Segment.SIZE / 2 + 2)
+private val zs = "z".repeat(Segment.SIZE / 2 + 3)
diff --git a/okio/src/jvmTest/kotlin/okio/Stopwatch.kt b/okio/src/jvmTest/kotlin/okio/Stopwatch.kt
new file mode 100644
index 00000000..4f4a0229
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/Stopwatch.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.within
+
+/** Stopwatch for asserting elapsed time during unit tests. */
+internal class Stopwatch {
+ private val start = System.nanoTime() / 1e9
+ private var offset = 0.0
+
+ /**
+ * Fails the test unless the time from the last assertion until now is `elapsed`, accepting
+ * differences in -200..+200 milliseconds.
+ */
+ fun assertElapsed(elapsed: Double) {
+ offset += elapsed
+ assertThat(System.nanoTime() / 1e9 - start).isCloseTo(offset, within(0.2))
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/TestUtil.kt b/okio/src/jvmTest/kotlin/okio/TestUtil.kt
new file mode 100644
index 00000000..f9f61e62
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/TestUtil.kt
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.ByteString.Companion.encodeUtf8
+import org.junit.Assume
+import java.io.IOException
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.io.Serializable
+import java.util.Random
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+object TestUtil {
+ // Necessary to make an internal member visible to Java.
+ @JvmField val SEGMENT_POOL_MAX_SIZE = SegmentPool.MAX_SIZE
+ const val SEGMENT_SIZE = Segment.SIZE
+ const val REPLACEMENT_CODE_POINT: Int = okio.REPLACEMENT_CODE_POINT
+
+ @JvmStatic fun segmentPoolByteCount() = SegmentPool.byteCount
+
+ @JvmStatic
+ fun segmentSizes(buffer: Buffer): List<Int> = okio.segmentSizes(buffer)
+
+ @JvmStatic
+ fun assertNoEmptySegments(buffer: Buffer) {
+ assertTrue(segmentSizes(buffer).all { it != 0 }, "Expected all segments to be non-empty")
+ }
+
+ @JvmStatic
+ fun assertByteArraysEquals(a: ByteArray, b: ByteArray) {
+ assertEquals(a.contentToString(), b.contentToString())
+ }
+
+ @JvmStatic
+ fun assertByteArrayEquals(expectedUtf8: String, b: ByteArray) {
+ assertEquals(expectedUtf8, b.toString(Charsets.UTF_8))
+ }
+
+ @JvmStatic
+ fun randomBytes(length: Int): ByteString {
+ val random = Random(0)
+ val randomBytes = ByteArray(length)
+ random.nextBytes(randomBytes)
+ return ByteString.of(*randomBytes)
+ }
+
+ @JvmStatic
+ fun randomSource(size: Long): Source {
+ return object : Source {
+ internal var random = Random(0)
+ internal var bytesLeft = size
+ internal var closed: Boolean = false
+
+ @Throws(IOException::class)
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ var byteCount = byteCount
+ if (closed) throw IllegalStateException("closed")
+ if (bytesLeft == 0L) return -1L
+ if (byteCount > bytesLeft) byteCount = bytesLeft
+
+ // If we can read a full segment we can save a copy.
+ if (byteCount >= Segment.SIZE) {
+ val segment = sink.writableSegment(Segment.SIZE)
+ random.nextBytes(segment.data)
+ segment.limit += Segment.SIZE
+ sink.size += Segment.SIZE.toLong()
+ bytesLeft -= Segment.SIZE.toLong()
+ return Segment.SIZE.toLong()
+ } else {
+ val data = ByteArray(byteCount.toInt())
+ random.nextBytes(data)
+ sink.write(data)
+ bytesLeft -= byteCount
+ return byteCount
+ }
+ }
+
+ override fun timeout() = Timeout.NONE
+
+ @Throws(IOException::class)
+ override fun close() {
+ closed = true
+ }
+ }
+ }
+
+ @JvmStatic
+ fun assertEquivalent(b1: ByteString, b2: ByteString) {
+ // Equals.
+ assertTrue(b1 == b2)
+ assertTrue(b1 == b1)
+ assertTrue(b2 == b1)
+
+ // Hash code.
+ assertEquals(b1.hashCode().toLong(), b2.hashCode().toLong())
+ assertEquals(b1.hashCode().toLong(), b1.hashCode().toLong())
+ assertEquals(b1.toString(), b2.toString())
+
+ // Content.
+ assertEquals(b1.size.toLong(), b2.size.toLong())
+ val b2Bytes = b2.toByteArray()
+ for (i in b2Bytes.indices) {
+ val b = b2Bytes[i]
+ assertEquals(b.toLong(), b1[i].toLong())
+ }
+ assertByteArraysEquals(b1.toByteArray(), b2Bytes)
+
+ // Doesn't equal a different byte string.
+ assertFalse(b1 == null)
+ assertFalse(b1 == Any())
+ if (b2Bytes.size > 0) {
+ val b3Bytes = b2Bytes.clone()
+ b3Bytes[b3Bytes.size - 1]++
+ val b3 = ByteString(b3Bytes)
+ assertFalse(b1 == b3)
+ assertFalse(b1.hashCode() == b3.hashCode())
+ } else {
+ val b3 = "a".encodeUtf8()
+ assertFalse(b1 == b3)
+ assertFalse(b1.hashCode() == b3.hashCode())
+ }
+ }
+
+ @JvmStatic
+ fun assertEquivalent(b1: Buffer, b2: Buffer) {
+ // Equals.
+ assertTrue(b1 == b2)
+ assertTrue(b1 == b1)
+ assertTrue(b2 == b1)
+
+ // Hash code.
+ assertEquals(b1.hashCode().toLong(), b2.hashCode().toLong())
+ assertEquals(b1.hashCode().toLong(), b1.hashCode().toLong())
+ assertEquals(b1.toString(), b2.toString())
+
+ // Content.
+ assertEquals(b1.size, b2.size)
+ val buffer = Buffer()
+ b2.copyTo(buffer, 0, b2.size)
+ val b2Bytes = b2.readByteArray()
+ for (i in b2Bytes.indices) {
+ val b = b2Bytes[i]
+ assertEquals(b.toLong(), b1[i.toLong()].toLong())
+ }
+
+ // Doesn't equal a different buffer.
+ assertFalse(b1 == Any())
+ if (b2Bytes.size > 0) {
+ val b3Bytes = b2Bytes.clone()
+ b3Bytes[b3Bytes.size - 1]++
+ val b3 = Buffer().write(b3Bytes)
+ assertFalse(b1 == b3)
+ assertFalse(b1.hashCode() == b3.hashCode())
+ } else {
+ val b3 = Buffer().writeUtf8("a")
+ assertFalse(b1 == b3)
+ assertFalse(b1.hashCode() == b3.hashCode())
+ }
+ }
+
+ /** Serializes original to bytes, then deserializes those bytes and returns the result. */
+ @Suppress("UNCHECKED_CAST")
+ @Throws(Exception::class)
+ @JvmStatic
+ // Assume serialization doesn't change types.
+ fun <T : Serializable> reserialize(original: T): T {
+ val buffer = Buffer()
+ val out = ObjectOutputStream(buffer.outputStream())
+ out.writeObject(original)
+ val input = ObjectInputStream(buffer.inputStream())
+ return input.readObject() as T
+ }
+
+ /**
+ * Returns a new buffer containing the data in `data` and a segment
+ * layout determined by `dice`.
+ */
+ @Throws(IOException::class)
+ @JvmStatic
+ fun bufferWithRandomSegmentLayout(dice: Random, data: ByteArray): Buffer {
+ val result = Buffer()
+
+ // Writing to result directly will yield packed segments. Instead, write to
+ // other buffers, then write those buffers to result.
+ var pos = 0
+ var byteCount: Int
+ while (pos < data.size) {
+ byteCount = Segment.SIZE / 2 + dice.nextInt(Segment.SIZE / 2)
+ if (byteCount > data.size - pos) byteCount = data.size - pos
+ val offset = dice.nextInt(Segment.SIZE - byteCount)
+
+ val segment = Buffer()
+ segment.write(ByteArray(offset))
+ segment.write(data, pos, byteCount)
+ segment.skip(offset.toLong())
+
+ result.write(segment, byteCount.toLong())
+ pos += byteCount
+ }
+
+ return result
+ }
+
+ /**
+ * Returns a new buffer containing the contents of `segments`, attempting to isolate each
+ * string to its own segment in the returned buffer. This clones buffers so that segments are
+ * shared, preventing compaction from occurring.
+ */
+ @Throws(Exception::class)
+ @JvmStatic
+ fun bufferWithSegments(vararg segments: String): Buffer {
+ val result = Buffer()
+ for (s in segments) {
+ val offsetInSegment = if (s.length < Segment.SIZE) (Segment.SIZE - s.length) / 2 else 0
+ val buffer = Buffer()
+ buffer.writeUtf8("_".repeat(offsetInSegment))
+ buffer.writeUtf8(s)
+ buffer.skip(offsetInSegment.toLong())
+ result.write(buffer.clone(), buffer.size)
+ }
+ return result
+ }
+
+ @JvmStatic
+ fun makeSegments(source: ByteString): ByteString {
+ val buffer = Buffer()
+ for (i in 0 until source.size) {
+ val segment = buffer.writableSegment(SEGMENT_SIZE)
+ segment.data[segment.pos] = source[i]
+ segment.limit++
+ buffer.size++
+ }
+ return buffer.snapshot()
+ }
+
+ /** Remove all segments from the pool and return them as a list. */
+ @JvmStatic
+ internal fun takeAllPoolSegments(): List<Segment> {
+ val result = mutableListOf<Segment>()
+ while (SegmentPool.byteCount > 0) {
+ result += SegmentPool.take()
+ }
+ return result
+ }
+
+ /** Returns a copy of `buffer` with no segments with `original`. */
+ @JvmStatic
+ fun deepCopy(original: Buffer): Buffer {
+ val result = Buffer()
+ if (original.size == 0L) return result
+
+ result.head = original.head!!.unsharedCopy()
+ result.head!!.prev = result.head
+ result.head!!.next = result.head!!.prev
+ var s = original.head!!.next
+ while (s !== original.head) {
+ result.head!!.prev!!.push(s!!.unsharedCopy())
+ s = s.next
+ }
+ result.size = original.size
+
+ return result
+ }
+
+ @JvmStatic
+ fun Int.reverseBytes(): Int {
+ /* ktlint-disable no-multi-spaces indent */
+ return (this and -0x1000000 ushr 24) or
+ (this and 0x00ff0000 ushr 8) or
+ (this and 0x0000ff00 shl 8) or
+ (this and 0x000000ff shl 24)
+ /* ktlint-enable no-multi-spaces indent */
+ }
+
+ @JvmStatic
+ fun Short.reverseBytes(): Short {
+ val i = toInt() and 0xffff
+ /* ktlint-disable no-multi-spaces indent */
+ val reversed = (i and 0xff00 ushr 8) or
+ (i and 0x00ff shl 8)
+ /* ktlint-enable no-multi-spaces indent */
+ return reversed.toShort()
+ }
+
+ fun assumeNotWindows() = Assume.assumeFalse(System.getProperty("os.name").toLowerCase().contains("win"))
+}
diff --git a/okio/src/jvmTest/kotlin/okio/ThrottlerTakeTest.kt b/okio/src/jvmTest/kotlin/okio/ThrottlerTakeTest.kt
new file mode 100644
index 00000000..aab94b30
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/ThrottlerTakeTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+
+class ThrottlerTakeTest {
+ private var nowNanos = 0L
+ private var elapsedNanos = 0L
+ private val throttler = Throttler(allocatedUntil = nowNanos)
+
+ @Test fun takeByByteCount() {
+ throttler.bytesPerSecond(bytesPerSecond = 20, waitByteCount = 5, maxByteCount = 10)
+
+ // We get the first 10 bytes immediately (that's maxByteCount).
+ assertThat(take(100L)).isEqualTo(10L)
+ assertElapsed(0L)
+
+ // Wait a quarter second for each subsequent 5 bytes (that's waitByteCount).
+ assertThat(take(100L)).isEqualTo(5L)
+ assertElapsed(250L)
+
+ assertThat(take(100L)).isEqualTo(5L)
+ assertElapsed(250L)
+
+ // Wait three quarters of a second to build up 15 bytes of potential.
+ // Since maxByteCount = 10, there will only be 10 bytes of potential.
+ sleep(750L)
+ assertElapsed(750L)
+
+ // We get 10 bytes immediately (that's maxByteCount again).
+ assertThat(take(100L)).isEqualTo(10L)
+ assertElapsed(0L)
+
+ // Wait a quarter second for each subsequent 5 bytes (that's waitByteCount again).
+ assertThat(take(100L)).isEqualTo(5L)
+ assertElapsed(250L)
+ }
+
+ @Test fun takeFullyTimeElapsed() {
+ throttler.bytesPerSecond(bytesPerSecond = 20, waitByteCount = 5, maxByteCount = 10)
+
+ // We write the first 10 bytes immediately (that's maxByteCount again).
+ takeFully(10L)
+ assertElapsed(0L)
+
+ // Wait a quarter second for each subsequent 5 bytes (that's waitByteCount).
+ takeFully(5L)
+ assertElapsed(250L)
+
+ // Wait a half second for 10 bytes.
+ takeFully(10L)
+ assertElapsed(500L)
+
+ // Wait a three quarters of a second to build up 15 bytes of potential.
+ // Since maxByteCount = 10, there will only be 10 bytes of potential.
+ sleep(750L)
+ assertElapsed(750L)
+
+ // We write the first 10 bytes immediately (that's maxByteCount again).
+ // Wait a quarter second for each subsequent 5 bytes (that's waitByteCount again).
+ takeFully(15L)
+ assertElapsed(250L)
+ }
+
+ @Test fun takeFullyWhenSaturated() {
+ throttler.bytesPerSecond(400L, 5L, 10L)
+
+ // Saturate the throttler.
+ assertThat(take(10L)).isEqualTo(10L)
+ assertElapsed(0L)
+
+ // At 400 bytes per second it takes 250 ms to read 100 bytes.
+ takeFully(100L)
+ assertElapsed(250L)
+ }
+
+ @Test fun takeFullyNoLimit() {
+ throttler.bytesPerSecond(0L, 5L, 10L)
+ takeFully(100L)
+ assertElapsed(0L)
+ }
+
+ /**
+ * We had a bug where integer division truncation would cause us to call wait() for 0 nanos. We
+ * fixed it by minimizing integer division generally, and by handling that case specifically.
+ */
+ @Test fun infiniteWait() {
+ throttler.bytesPerSecond(3, maxByteCount = 4, waitByteCount = 4)
+ takeFully(7)
+ assertElapsed(1000L)
+ }
+
+ /** Take at least the minimum and up to `byteCount` bytes, sleeping once if necessary. */
+ private fun take(byteCount: Long): Long {
+ val byteCountOrWaitNanos = throttler.byteCountOrWaitNanos(nowNanos, byteCount)
+ if (byteCountOrWaitNanos >= 0L) return byteCountOrWaitNanos
+
+ nowNanos += -byteCountOrWaitNanos
+
+ val resultAfterWait = throttler.byteCountOrWaitNanos(nowNanos, byteCount)
+ assertThat(resultAfterWait).isGreaterThan(0L)
+ return resultAfterWait
+ }
+
+ /** Take all of `byteCount` bytes, advancing the clock until they're all taken. */
+ private fun takeFully(byteCount: Long) {
+ var remaining = byteCount
+ while (remaining > 0L) {
+ val byteCountOrWaitNanos = throttler.byteCountOrWaitNanos(nowNanos, remaining)
+ if (byteCountOrWaitNanos >= 0L) {
+ remaining -= byteCountOrWaitNanos
+ } else {
+ nowNanos += -byteCountOrWaitNanos
+ }
+ }
+ }
+
+ private fun assertElapsed(millis: Long) {
+ elapsedNanos += TimeUnit.MILLISECONDS.toNanos(millis)
+ assertThat(nowNanos).isEqualTo(elapsedNanos)
+ }
+
+ private fun sleep(millis: Long) {
+ nowNanos += TimeUnit.MILLISECONDS.toNanos(millis)
+ }
+}
diff --git a/okio/src/jvmTest/kotlin/okio/ThrottlerTest.kt b/okio/src/jvmTest/kotlin/okio/ThrottlerTest.kt
new file mode 100644
index 00000000..0b151794
--- /dev/null
+++ b/okio/src/jvmTest/kotlin/okio/ThrottlerTest.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.TestUtil.randomSource
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.Executors
+import kotlin.test.Ignore
+
+@Ignore("These tests are flaky and fail on slower hardware, need to be improved")
+class ThrottlerTest {
+ private val size = 1024L * 80L // 80 KiB
+ private val source = randomSource(size)
+
+ private val throttler = Throttler()
+ private val throttlerSlow = Throttler()
+
+ private val threads = 4
+ private val executorService = Executors.newFixedThreadPool(threads)
+ private var stopwatch = Stopwatch()
+
+ @Before fun setup() {
+ throttler.bytesPerSecond(4 * size, 4096, 8192)
+ throttlerSlow.bytesPerSecond(2 * size, 4096, 8192)
+ stopwatch = Stopwatch()
+ }
+
+ @After fun teardown() {
+ executorService.shutdown()
+ }
+
+ @Test fun source() {
+ throttler.source(source).buffer().readAll(blackholeSink())
+ stopwatch.assertElapsed(0.25)
+ }
+
+ @Test fun sink() {
+ source.buffer().readAll(throttler.sink(blackholeSink()))
+ stopwatch.assertElapsed(0.25)
+ }
+
+ @Test fun doubleSourceThrottle() {
+ throttler.source(throttler.source(source)).buffer().readAll(blackholeSink())
+ stopwatch.assertElapsed(0.5)
+ }
+
+ @Test fun doubleSinkThrottle() {
+ source.buffer().readAll(throttler.sink(throttler.sink(blackholeSink())))
+ stopwatch.assertElapsed(0.5)
+ }
+
+ @Test fun singleSourceMultiThrottleSlowerThenSlow() {
+ source.buffer().readAll(throttler.sink(throttlerSlow.sink(blackholeSink())))
+ stopwatch.assertElapsed(0.5)
+ }
+
+ @Test fun singleSourceMultiThrottleSlowThenSlower() {
+ source.buffer().readAll(throttlerSlow.sink(throttler.sink(blackholeSink())))
+ stopwatch.assertElapsed(0.5)
+ }
+
+ @Test fun slowSourceSlowerSink() {
+ throttler.source(source).buffer().readAll(throttlerSlow.sink(blackholeSink()))
+ stopwatch.assertElapsed(0.5)
+ }
+
+ @Test fun slowSinkSlowerSource() {
+ throttlerSlow.source(source).buffer().readAll(throttler.sink(blackholeSink()))
+ stopwatch.assertElapsed(0.5)
+ }
+
+ @Test fun parallel() {
+ val futures = List(threads) {
+ executorService.submit {
+ val source = randomSource(size)
+ source.buffer().readAll(throttler.sink(blackholeSink()))
+ }
+ }
+ for (future in futures) {
+ future.get()
+ }
+ stopwatch.assertElapsed(1.0)
+ }
+
+ @Test fun parallelFastThenSlower() {
+ val futures = List(threads) {
+ executorService.submit {
+ val source = randomSource(size)
+ source.buffer().readAll(throttler.sink(blackholeSink()))
+ }
+ }
+ Thread.sleep(500)
+ throttler.bytesPerSecond(2 * size)
+ for (future in futures) {
+ future.get()
+ }
+ stopwatch.assertElapsed(1.5)
+ }
+
+ @Test fun parallelSlowThenFaster() {
+ val futures = List(threads) {
+ executorService.submit {
+ val source = randomSource(size)
+ source.buffer().readAll(throttlerSlow.sink(blackholeSink()))
+ }
+ }
+ Thread.sleep(1_000)
+ throttlerSlow.bytesPerSecond(4 * size)
+ for (future in futures) {
+ future.get()
+ }
+ stopwatch.assertElapsed(1.5)
+ }
+
+ @Test fun parallelIndividualThrottle() {
+ val futures = List(threads) {
+ executorService.submit {
+ val throttlerLocal = Throttler()
+ throttlerLocal.bytesPerSecond(4 * size, maxByteCount = 8192)
+
+ val source = randomSource(size)
+ source.buffer().readAll(throttlerLocal.sink(blackholeSink()))
+ }
+ }
+ for (future in futures) {
+ future.get()
+ }
+ stopwatch.assertElapsed(0.25)
+ }
+
+ @Test fun parallelGroupAndIndividualThrottle() {
+ val futures = List(threads) {
+ executorService.submit {
+ val throttlerLocal = Throttler()
+ throttlerLocal.bytesPerSecond(4 * size, maxByteCount = 8192)
+
+ val source = randomSource(size)
+ source.buffer().readAll(throttler.sink(throttlerLocal.sink(blackholeSink())))
+ }
+ }
+ for (future in futures) {
+ future.get()
+ }
+ stopwatch.assertElapsed(1.0)
+ }
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/-Platform.kt b/okio/src/nonJvmMain/kotlin/okio/-Platform.kt
new file mode 100644
index 00000000..90fcd36d
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/-Platform.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.internal.commonAsUtf8ToByteArray
+import okio.internal.commonToUtf8String
+
+internal actual fun ByteArray.toUtf8String(): String = commonToUtf8String()
+
+internal actual fun String.asUtf8ToByteArray(): ByteArray = commonAsUtf8ToByteArray()
+
+actual open class ArrayIndexOutOfBoundsException actual constructor(
+ message: String?
+) : IndexOutOfBoundsException(message)
+
+internal actual inline fun <R> synchronized(lock: Any, block: () -> R): R = block()
+
+actual open class IOException actual constructor(
+ message: String?,
+ cause: Throwable?
+) : Exception(message, cause) {
+ actual constructor(message: String?) : this(message, null)
+}
+
+actual open class EOFException actual constructor(message: String?) : IOException(message)
+
+actual open class FileNotFoundException actual constructor(message: String?) : IOException(message)
+
+actual interface Closeable {
+ @Throws(IOException::class)
+ actual fun close()
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/Buffer.kt b/okio/src/nonJvmMain/kotlin/okio/Buffer.kt
new file mode 100644
index 00000000..ec28f63f
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/Buffer.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.HashFunction
+import okio.internal.Hmac
+import okio.internal.Md5
+import okio.internal.Sha1
+import okio.internal.Sha256
+import okio.internal.Sha512
+import okio.internal.commonClear
+import okio.internal.commonClose
+import okio.internal.commonCompleteSegmentByteCount
+import okio.internal.commonCopy
+import okio.internal.commonCopyTo
+import okio.internal.commonEquals
+import okio.internal.commonExpandBuffer
+import okio.internal.commonGet
+import okio.internal.commonHashCode
+import okio.internal.commonIndexOf
+import okio.internal.commonIndexOfElement
+import okio.internal.commonNext
+import okio.internal.commonRangeEquals
+import okio.internal.commonRead
+import okio.internal.commonReadAll
+import okio.internal.commonReadAndWriteUnsafe
+import okio.internal.commonReadByte
+import okio.internal.commonReadByteArray
+import okio.internal.commonReadByteString
+import okio.internal.commonReadDecimalLong
+import okio.internal.commonReadFully
+import okio.internal.commonReadHexadecimalUnsignedLong
+import okio.internal.commonReadInt
+import okio.internal.commonReadLong
+import okio.internal.commonReadShort
+import okio.internal.commonReadUnsafe
+import okio.internal.commonReadUtf8
+import okio.internal.commonReadUtf8CodePoint
+import okio.internal.commonReadUtf8Line
+import okio.internal.commonReadUtf8LineStrict
+import okio.internal.commonResizeBuffer
+import okio.internal.commonSeek
+import okio.internal.commonSelect
+import okio.internal.commonSkip
+import okio.internal.commonSnapshot
+import okio.internal.commonWritableSegment
+import okio.internal.commonWrite
+import okio.internal.commonWriteAll
+import okio.internal.commonWriteByte
+import okio.internal.commonWriteDecimalLong
+import okio.internal.commonWriteHexadecimalUnsignedLong
+import okio.internal.commonWriteInt
+import okio.internal.commonWriteLong
+import okio.internal.commonWriteShort
+import okio.internal.commonWriteUtf8
+import okio.internal.commonWriteUtf8CodePoint
+
+actual class Buffer : BufferedSource, BufferedSink {
+ internal actual var head: Segment? = null
+
+ actual var size: Long = 0L
+ internal set
+
+ actual override val buffer: Buffer get() = this
+
+ actual override fun emitCompleteSegments(): Buffer = this // Nowhere to emit to!
+
+ actual override fun emit(): Buffer = this // Nowhere to emit to!
+
+ override fun exhausted(): Boolean = size == 0L
+
+ override fun require(byteCount: Long) {
+ if (size < byteCount) throw EOFException(null)
+ }
+
+ override fun request(byteCount: Long): Boolean = size >= byteCount
+
+ override fun peek(): BufferedSource = PeekSource(this).buffer()
+
+ actual fun copyTo(
+ out: Buffer,
+ offset: Long,
+ byteCount: Long
+ ): Buffer = commonCopyTo(out, offset, byteCount)
+
+ actual fun copyTo(
+ out: Buffer,
+ offset: Long
+ ): Buffer = copyTo(out, offset, size - offset)
+
+ actual operator fun get(pos: Long): Byte = commonGet(pos)
+
+ actual fun completeSegmentByteCount(): Long = commonCompleteSegmentByteCount()
+
+ override fun readByte(): Byte = commonReadByte()
+
+ override fun readShort(): Short = commonReadShort()
+
+ override fun readInt(): Int = commonReadInt()
+
+ override fun readLong(): Long = commonReadLong()
+
+ override fun readShortLe(): Short = readShort().reverseBytes()
+
+ override fun readIntLe(): Int = readInt().reverseBytes()
+
+ override fun readLongLe(): Long = readLong().reverseBytes()
+
+ override fun readDecimalLong(): Long = commonReadDecimalLong()
+
+ override fun readHexadecimalUnsignedLong(): Long = commonReadHexadecimalUnsignedLong()
+
+ override fun readByteString(): ByteString = commonReadByteString()
+
+ override fun readByteString(byteCount: Long): ByteString = commonReadByteString(byteCount)
+
+ override fun readFully(sink: Buffer, byteCount: Long): Unit = commonReadFully(sink, byteCount)
+
+ override fun readAll(sink: Sink): Long = commonReadAll(sink)
+
+ override fun readUtf8(): String = readUtf8(size)
+
+ override fun readUtf8(byteCount: Long): String = commonReadUtf8(byteCount)
+
+ override fun readUtf8Line(): String? = commonReadUtf8Line()
+
+ override fun readUtf8LineStrict(): String = readUtf8LineStrict(Long.MAX_VALUE)
+
+ override fun readUtf8LineStrict(limit: Long): String = commonReadUtf8LineStrict(limit)
+
+ override fun readUtf8CodePoint(): Int = commonReadUtf8CodePoint()
+
+ override fun select(options: Options): Int = commonSelect(options)
+
+ override fun readByteArray(): ByteArray = commonReadByteArray()
+
+ override fun readByteArray(byteCount: Long): ByteArray = commonReadByteArray(byteCount)
+
+ override fun read(sink: ByteArray): Int = commonRead(sink)
+
+ override fun readFully(sink: ByteArray): Unit = commonReadFully(sink)
+
+ override fun read(sink: ByteArray, offset: Int, byteCount: Int): Int =
+ commonRead(sink, offset, byteCount)
+
+ actual fun clear(): Unit = commonClear()
+
+ actual override fun skip(byteCount: Long): Unit = commonSkip(byteCount)
+
+ actual override fun write(byteString: ByteString): Buffer = commonWrite(byteString)
+
+ actual override fun write(byteString: ByteString, offset: Int, byteCount: Int) =
+ commonWrite(byteString, offset, byteCount)
+
+ internal actual fun writableSegment(minimumCapacity: Int): Segment =
+ commonWritableSegment(minimumCapacity)
+
+ actual override fun writeUtf8(string: String): Buffer = writeUtf8(string, 0, string.length)
+
+ actual override fun writeUtf8(string: String, beginIndex: Int, endIndex: Int): Buffer =
+ commonWriteUtf8(string, beginIndex, endIndex)
+
+ actual override fun writeUtf8CodePoint(codePoint: Int): Buffer =
+ commonWriteUtf8CodePoint(codePoint)
+
+ actual override fun write(source: ByteArray): Buffer = commonWrite(source)
+
+ actual override fun write(source: ByteArray, offset: Int, byteCount: Int): Buffer =
+ commonWrite(source, offset, byteCount)
+
+ override fun writeAll(source: Source): Long = commonWriteAll(source)
+
+ actual override fun write(source: Source, byteCount: Long): Buffer =
+ commonWrite(source, byteCount)
+
+ actual override fun writeByte(b: Int): Buffer = commonWriteByte(b)
+
+ actual override fun writeShort(s: Int): Buffer = commonWriteShort(s)
+
+ actual override fun writeShortLe(s: Int): Buffer = writeShort(s.toShort().reverseBytes().toInt())
+
+ actual override fun writeInt(i: Int): Buffer = commonWriteInt(i)
+
+ actual override fun writeIntLe(i: Int): Buffer = writeInt(i.reverseBytes())
+
+ actual override fun writeLong(v: Long): Buffer = commonWriteLong(v)
+
+ actual override fun writeLongLe(v: Long): Buffer = writeLong(v.reverseBytes())
+
+ actual override fun writeDecimalLong(v: Long): Buffer = commonWriteDecimalLong(v)
+
+ actual override fun writeHexadecimalUnsignedLong(v: Long): Buffer =
+ commonWriteHexadecimalUnsignedLong(v)
+
+ override fun write(source: Buffer, byteCount: Long): Unit = commonWrite(source, byteCount)
+
+ override fun read(sink: Buffer, byteCount: Long): Long = commonRead(sink, byteCount)
+
+ override fun indexOf(b: Byte): Long = indexOf(b, 0, Long.MAX_VALUE)
+
+ override fun indexOf(b: Byte, fromIndex: Long): Long = indexOf(b, fromIndex, Long.MAX_VALUE)
+
+ override fun indexOf(b: Byte, fromIndex: Long, toIndex: Long): Long =
+ commonIndexOf(b, fromIndex, toIndex)
+
+ override fun indexOf(bytes: ByteString): Long = indexOf(bytes, 0)
+
+ override fun indexOf(bytes: ByteString, fromIndex: Long): Long = commonIndexOf(bytes, fromIndex)
+
+ override fun indexOfElement(targetBytes: ByteString): Long = indexOfElement(targetBytes, 0L)
+
+ override fun indexOfElement(targetBytes: ByteString, fromIndex: Long): Long =
+ commonIndexOfElement(targetBytes, fromIndex)
+
+ override fun rangeEquals(offset: Long, bytes: ByteString): Boolean =
+ rangeEquals(offset, bytes, 0, bytes.size)
+
+ override fun rangeEquals(
+ offset: Long,
+ bytes: ByteString,
+ bytesOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, bytes, bytesOffset, byteCount)
+
+ override fun flush() = Unit
+
+ override fun close() = Unit
+
+ override fun timeout(): Timeout = Timeout.NONE
+
+ override fun equals(other: Any?): Boolean = commonEquals(other)
+
+ override fun hashCode(): Int = commonHashCode()
+
+ /**
+ * Returns a human-readable string that describes the contents of this buffer. Typically this
+ * is a string like `[text=Hello]` or `[hex=0000ffff]`.
+ */
+ override fun toString() = snapshot().toString()
+
+ actual fun copy(): Buffer = commonCopy()
+
+ actual fun snapshot(): ByteString = commonSnapshot()
+
+ actual fun snapshot(byteCount: Int): ByteString = commonSnapshot(byteCount)
+
+ actual fun md5() = digest(Md5())
+
+ actual fun sha1() = digest(Sha1())
+
+ actual fun sha256() = digest(Sha256())
+
+ actual fun sha512() = digest(Sha512())
+
+ /** Returns the 160-bit SHA-1 HMAC of this buffer. */
+ actual fun hmacSha1(key: ByteString) = digest(Hmac.sha1(key))
+
+ /** Returns the 256-bit SHA-256 HMAC of this buffer. */
+ actual fun hmacSha256(key: ByteString) = digest(Hmac.sha256(key))
+
+ /** Returns the 512-bit SHA-512 HMAC of this buffer. */
+ actual fun hmacSha512(key: ByteString) = digest(Hmac.sha512(key))
+
+ private fun digest(hash: HashFunction): ByteString {
+ forEachSegment { segment ->
+ hash.update(segment.data, segment.pos, segment.limit - segment.pos)
+ }
+
+ return ByteString(hash.digest())
+ }
+
+ private fun forEachSegment(action: (Segment) -> Unit) {
+ head?.let { head ->
+ var segment: Segment? = head
+ do {
+ segment?.let(action)
+ segment = segment?.next
+ } while (segment !== head)
+ }
+ }
+
+ actual fun readUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor = commonReadUnsafe(unsafeCursor)
+
+ actual fun readAndWriteUnsafe(unsafeCursor: UnsafeCursor): UnsafeCursor =
+ commonReadAndWriteUnsafe(unsafeCursor)
+
+ actual class UnsafeCursor {
+ actual var buffer: Buffer? = null
+ actual var readWrite: Boolean = false
+
+ internal actual var segment: Segment? = null
+ actual var offset = -1L
+ actual var data: ByteArray? = null
+ actual var start = -1
+ actual var end = -1
+
+ actual fun next(): Int = commonNext()
+
+ actual fun seek(offset: Long): Int = commonSeek(offset)
+
+ actual fun resizeBuffer(newSize: Long): Long = commonResizeBuffer(newSize)
+
+ actual fun expandBuffer(minByteCount: Int): Long = commonExpandBuffer(minByteCount)
+
+ actual fun close() {
+ commonClose()
+ }
+ }
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/BufferedSink.kt b/okio/src/nonJvmMain/kotlin/okio/BufferedSink.kt
new file mode 100644
index 00000000..65d717c6
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/BufferedSink.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+actual interface BufferedSink : Sink {
+ actual val buffer: Buffer
+
+ actual fun write(byteString: ByteString): BufferedSink
+
+ actual fun write(byteString: ByteString, offset: Int, byteCount: Int): BufferedSink
+
+ actual fun write(source: ByteArray): BufferedSink
+
+ actual fun write(source: ByteArray, offset: Int, byteCount: Int): BufferedSink
+
+ actual fun writeAll(source: Source): Long
+
+ actual fun write(source: Source, byteCount: Long): BufferedSink
+
+ actual fun writeUtf8(string: String): BufferedSink
+
+ actual fun writeUtf8(string: String, beginIndex: Int, endIndex: Int): BufferedSink
+
+ actual fun writeUtf8CodePoint(codePoint: Int): BufferedSink
+
+ actual fun writeByte(b: Int): BufferedSink
+
+ actual fun writeShort(s: Int): BufferedSink
+
+ actual fun writeShortLe(s: Int): BufferedSink
+
+ actual fun writeInt(i: Int): BufferedSink
+
+ actual fun writeIntLe(i: Int): BufferedSink
+
+ actual fun writeLong(v: Long): BufferedSink
+
+ actual fun writeLongLe(v: Long): BufferedSink
+
+ actual fun writeDecimalLong(v: Long): BufferedSink
+
+ actual fun writeHexadecimalUnsignedLong(v: Long): BufferedSink
+
+ actual fun emit(): BufferedSink
+
+ actual fun emitCompleteSegments(): BufferedSink
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/BufferedSource.kt b/okio/src/nonJvmMain/kotlin/okio/BufferedSource.kt
new file mode 100644
index 00000000..98b7718a
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/BufferedSource.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+actual interface BufferedSource : Source {
+ actual val buffer: Buffer
+
+ actual fun exhausted(): Boolean
+
+ actual fun require(byteCount: Long)
+
+ actual fun request(byteCount: Long): Boolean
+
+ actual fun readByte(): Byte
+
+ actual fun readShort(): Short
+
+ actual fun readShortLe(): Short
+
+ actual fun readInt(): Int
+
+ actual fun readIntLe(): Int
+
+ actual fun readLong(): Long
+
+ actual fun readLongLe(): Long
+
+ actual fun readDecimalLong(): Long
+
+ actual fun readHexadecimalUnsignedLong(): Long
+
+ actual fun skip(byteCount: Long)
+
+ actual fun readByteString(): ByteString
+
+ actual fun readByteString(byteCount: Long): ByteString
+
+ actual fun select(options: Options): Int
+
+ actual fun readByteArray(): ByteArray
+
+ actual fun readByteArray(byteCount: Long): ByteArray
+
+ actual fun read(sink: ByteArray): Int
+
+ actual fun readFully(sink: ByteArray)
+
+ actual fun read(sink: ByteArray, offset: Int, byteCount: Int): Int
+
+ actual fun readFully(sink: Buffer, byteCount: Long)
+
+ actual fun readAll(sink: Sink): Long
+
+ actual fun readUtf8(): String
+
+ actual fun readUtf8(byteCount: Long): String
+
+ actual fun readUtf8Line(): String?
+
+ actual fun readUtf8LineStrict(): String
+
+ actual fun readUtf8LineStrict(limit: Long): String
+
+ actual fun readUtf8CodePoint(): Int
+
+ actual fun indexOf(b: Byte): Long
+
+ actual fun indexOf(b: Byte, fromIndex: Long): Long
+
+ actual fun indexOf(b: Byte, fromIndex: Long, toIndex: Long): Long
+
+ actual fun indexOf(bytes: ByteString): Long
+
+ actual fun indexOf(bytes: ByteString, fromIndex: Long): Long
+
+ actual fun indexOfElement(targetBytes: ByteString): Long
+
+ actual fun indexOfElement(targetBytes: ByteString, fromIndex: Long): Long
+
+ actual fun rangeEquals(offset: Long, bytes: ByteString): Boolean
+
+ actual fun rangeEquals(offset: Long, bytes: ByteString, bytesOffset: Int, byteCount: Int): Boolean
+
+ actual fun peek(): BufferedSource
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/ByteString.kt b/okio/src/nonJvmMain/kotlin/okio/ByteString.kt
new file mode 100644
index 00000000..f0f038b6
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/ByteString.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.internal.HashFunction
+import okio.internal.Hmac
+import okio.internal.Md5
+import okio.internal.Sha1
+import okio.internal.Sha256
+import okio.internal.Sha512
+import okio.internal.commonBase64
+import okio.internal.commonBase64Url
+import okio.internal.commonCompareTo
+import okio.internal.commonDecodeBase64
+import okio.internal.commonDecodeHex
+import okio.internal.commonEncodeUtf8
+import okio.internal.commonEndsWith
+import okio.internal.commonEquals
+import okio.internal.commonGetByte
+import okio.internal.commonGetSize
+import okio.internal.commonHashCode
+import okio.internal.commonHex
+import okio.internal.commonIndexOf
+import okio.internal.commonInternalArray
+import okio.internal.commonLastIndexOf
+import okio.internal.commonOf
+import okio.internal.commonRangeEquals
+import okio.internal.commonStartsWith
+import okio.internal.commonSubstring
+import okio.internal.commonToAsciiLowercase
+import okio.internal.commonToAsciiUppercase
+import okio.internal.commonToByteArray
+import okio.internal.commonToByteString
+import okio.internal.commonToString
+import okio.internal.commonUtf8
+import okio.internal.commonWrite
+
+actual open class ByteString
+internal actual constructor(
+ internal actual val data: ByteArray
+) : Comparable<ByteString> {
+ @Suppress("SetterBackingFieldAssignment")
+ internal actual var hashCode: Int = 0 // 0 if unknown.
+ set(value) {
+ // Do nothing to avoid IllegalImmutabilityException.
+ }
+ @Suppress("SetterBackingFieldAssignment")
+ internal actual var utf8: String? = null
+ set(value) {
+ // Do nothing to avoid IllegalImmutabilityException.
+ }
+
+ actual open fun utf8(): String = commonUtf8()
+
+ actual open fun base64(): String = commonBase64()
+
+ actual open fun base64Url(): String = commonBase64Url()
+
+ actual open fun hex(): String = commonHex()
+
+ actual fun md5() = digest(Md5())
+
+ actual fun sha1() = digest(Sha1())
+
+ actual fun sha256() = digest(Sha256())
+
+ actual fun sha512() = digest(Sha512())
+
+ /** Returns the 160-bit SHA-1 HMAC of this byte string. */
+ actual fun hmacSha1(key: ByteString) = digest(Hmac.sha1(key))
+
+ /** Returns the 256-bit SHA-256 HMAC of this byte string. */
+ actual fun hmacSha256(key: ByteString) = digest(Hmac.sha256(key))
+
+ /** Returns the 512-bit SHA-512 HMAC of this byte string. */
+ actual fun hmacSha512(key: ByteString) = digest(Hmac.sha512(key))
+
+ internal open fun digest(hashFunction: HashFunction): ByteString {
+ hashFunction.update(data, 0, size)
+ val digestBytes = hashFunction.digest()
+ return ByteString(digestBytes)
+ }
+
+ actual open fun toAsciiLowercase(): ByteString = commonToAsciiLowercase()
+
+ actual open fun toAsciiUppercase(): ByteString = commonToAsciiUppercase()
+
+ actual open fun substring(beginIndex: Int, endIndex: Int): ByteString =
+ commonSubstring(beginIndex, endIndex)
+
+ internal actual open fun internalGet(pos: Int): Byte {
+ if (pos >= size || pos < 0) throw ArrayIndexOutOfBoundsException("size=$size pos=$pos")
+ return commonGetByte(pos)
+ }
+
+ actual operator fun get(index: Int): Byte = internalGet(index)
+
+ actual val size
+ get() = getSize()
+
+ internal actual open fun getSize() = commonGetSize()
+
+ actual open fun toByteArray() = commonToByteArray()
+
+ internal actual open fun internalArray() = commonInternalArray()
+
+ internal actual open fun write(buffer: Buffer, offset: Int, byteCount: Int) =
+ commonWrite(buffer, offset, byteCount)
+
+ actual open fun rangeEquals(
+ offset: Int,
+ other: ByteString,
+ otherOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount)
+
+ actual open fun rangeEquals(
+ offset: Int,
+ other: ByteArray,
+ otherOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount)
+
+ actual fun startsWith(prefix: ByteString) = commonStartsWith(prefix)
+
+ actual fun startsWith(prefix: ByteArray) = commonStartsWith(prefix)
+
+ actual fun endsWith(suffix: ByteString) = commonEndsWith(suffix)
+
+ actual fun endsWith(suffix: ByteArray) = commonEndsWith(suffix)
+
+ actual fun indexOf(other: ByteString, fromIndex: Int) = indexOf(other.internalArray(), fromIndex)
+
+ actual open fun indexOf(other: ByteArray, fromIndex: Int) = commonIndexOf(other, fromIndex)
+
+ actual fun lastIndexOf(other: ByteString, fromIndex: Int) = commonLastIndexOf(other, fromIndex)
+
+ actual open fun lastIndexOf(other: ByteArray, fromIndex: Int) = commonLastIndexOf(other, fromIndex)
+
+ actual override fun equals(other: Any?) = commonEquals(other)
+
+ actual override fun hashCode() = commonHashCode()
+
+ actual override fun compareTo(other: ByteString) = commonCompareTo(other)
+
+ /**
+ * Returns a human-readable string that describes the contents of this byte string. Typically this
+ * is a string like `[text=Hello]` or `[hex=0000ffff]`.
+ */
+ actual override fun toString() = commonToString()
+
+ actual companion object {
+ actual val EMPTY: ByteString = ByteString(byteArrayOf())
+
+ actual fun of(vararg data: Byte) = commonOf(data)
+
+ actual fun ByteArray.toByteString(offset: Int, byteCount: Int): ByteString =
+ commonToByteString(offset, byteCount)
+
+ actual fun String.encodeUtf8(): ByteString = commonEncodeUtf8()
+
+ actual fun String.decodeBase64(): ByteString? = commonDecodeBase64()
+
+ actual fun String.decodeHex() = commonDecodeHex()
+ }
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/HashingSink.kt b/okio/src/nonJvmMain/kotlin/okio/HashingSink.kt
new file mode 100644
index 00000000..cbd14a26
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/HashingSink.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.HashFunction
+import okio.internal.Hmac
+import okio.internal.Md5
+import okio.internal.Sha1
+import okio.internal.Sha256
+import okio.internal.Sha512
+
+actual class HashingSink internal constructor(
+ private val sink: Sink,
+ private val hashFunction: HashFunction
+) : Sink {
+
+ override fun write(source: Buffer, byteCount: Long) {
+ checkOffsetAndCount(source.size, 0, byteCount)
+
+ // Hash byteCount bytes from the prefix of source.
+ var hashedCount = 0L
+ var s = source.head!!
+ while (hashedCount < byteCount) {
+ val toHash = minOf(byteCount - hashedCount, s.limit - s.pos).toInt()
+ hashFunction.update(s.data, s.pos, toHash)
+ hashedCount += toHash
+ s = s.next!!
+ }
+
+ // Write those bytes to the sink.
+ sink.write(source, byteCount)
+ }
+
+ override fun flush() = sink.flush()
+
+ override fun timeout(): Timeout = sink.timeout()
+
+ override fun close() = sink.close()
+
+ /**
+ * Returns the hash of the bytes accepted thus far and resets the internal state of this sink.
+ *
+ * **Warning:** This method is not idempotent. Each time this method is called its
+ * internal state is cleared. This starts a new hash with zero bytes accepted.
+ */
+ actual val hash: ByteString
+ get() {
+ val result = hashFunction.digest()
+ return ByteString(result)
+ }
+
+ actual companion object {
+
+ /** Returns a sink that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */
+ actual fun md5(sink: Sink) = HashingSink(sink, Md5())
+
+ /** Returns a sink that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */
+ actual fun sha1(sink: Sink) = HashingSink(sink, Sha1())
+
+ /** Returns a sink that uses the SHA-256 hash algorithm to produce 256-bit hashes. */
+ actual fun sha256(sink: Sink) = HashingSink(sink, Sha256())
+
+ /** Returns a sink that uses the SHA-512 hash algorithm to produce 512-bit hashes. */
+ actual fun sha512(sink: Sink) = HashingSink(sink, Sha512())
+
+ /** Returns a sink that uses the obsolete SHA-1 HMAC algorithm to produce 160-bit hashes. */
+ actual fun hmacSha1(sink: Sink, key: ByteString) = HashingSink(sink, Hmac.sha1(key))
+
+ /** Returns a sink that uses the SHA-256 HMAC algorithm to produce 256-bit hashes. */
+ actual fun hmacSha256(sink: Sink, key: ByteString) = HashingSink(sink, Hmac.sha256(key))
+
+ /** Returns a sink that uses the SHA-512 HMAC algorithm to produce 512-bit hashes. */
+ actual fun hmacSha512(sink: Sink, key: ByteString) = HashingSink(sink, Hmac.sha512(key))
+ }
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/HashingSource.kt b/okio/src/nonJvmMain/kotlin/okio/HashingSource.kt
new file mode 100644
index 00000000..62bfd608
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/HashingSource.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.HashFunction
+import okio.internal.Hmac
+import okio.internal.Md5
+import okio.internal.Sha1
+import okio.internal.Sha256
+import okio.internal.Sha512
+
+actual class HashingSource internal constructor(
+ private val source: Source,
+ private val hashFunction: HashFunction
+) : Source {
+
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ val result = source.read(sink, byteCount)
+
+ if (result != -1L) {
+ var start = sink.size - result
+
+ // Find the first segment that has new bytes.
+ var offset = sink.size
+ var s = sink.head!!
+ while (offset > start) {
+ s = s.prev!!
+ offset -= (s.limit - s.pos).toLong()
+ }
+
+ // Hash that segment and all the rest until the end.
+ while (offset < sink.size) {
+ val pos = (s.pos + start - offset).toInt()
+ hashFunction.update(s.data, pos, s.limit - pos)
+ offset += s.limit - s.pos
+ start = offset
+ s = s.next!!
+ }
+ }
+
+ return result
+ }
+
+ override fun timeout(): Timeout =
+ source.timeout()
+
+ override fun close() =
+ source.close()
+
+ actual val hash: ByteString
+ get() {
+ val result = hashFunction.digest()
+ return ByteString(result)
+ }
+
+ actual companion object {
+
+ /** Returns a source that uses the obsolete MD5 hash algorithm to produce 128-bit hashes. */
+ actual fun md5(source: Source) = HashingSource(source, Md5())
+
+ /** Returns a source that uses the obsolete SHA-1 hash algorithm to produce 160-bit hashes. */
+ actual fun sha1(source: Source) = HashingSource(source, Sha1())
+
+ /** Returns a source that uses the SHA-256 hash algorithm to produce 256-bit hashes. */
+ actual fun sha256(source: Source) = HashingSource(source, Sha256())
+
+ /** Returns a source that uses the SHA-512 hash algorithm to produce 512-bit hashes. */
+ actual fun sha512(source: Source) = HashingSource(source, Sha512())
+
+ /** Returns a source that uses the obsolete SHA-1 HMAC algorithm to produce 160-bit hashes. */
+ actual fun hmacSha1(source: Source, key: ByteString) = HashingSource(source, Hmac.sha1(key))
+
+ /** Returns a source that uses the SHA-256 HMAC algorithm to produce 256-bit hashes. */
+ actual fun hmacSha256(source: Source, key: ByteString) = HashingSource(source, Hmac.sha256(key))
+
+ /** Returns a source that uses the SHA-512 HMAC algorithm to produce 512-bit hashes. */
+ actual fun hmacSha512(source: Source, key: ByteString) = HashingSource(source, Hmac.sha512(key))
+ }
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/RealBufferedSink.kt b/okio/src/nonJvmMain/kotlin/okio/RealBufferedSink.kt
new file mode 100644
index 00000000..ed03094e
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/RealBufferedSink.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package okio
+
+import okio.internal.commonClose
+import okio.internal.commonEmit
+import okio.internal.commonEmitCompleteSegments
+import okio.internal.commonFlush
+import okio.internal.commonTimeout
+import okio.internal.commonToString
+import okio.internal.commonWrite
+import okio.internal.commonWriteAll
+import okio.internal.commonWriteByte
+import okio.internal.commonWriteDecimalLong
+import okio.internal.commonWriteHexadecimalUnsignedLong
+import okio.internal.commonWriteInt
+import okio.internal.commonWriteIntLe
+import okio.internal.commonWriteLong
+import okio.internal.commonWriteLongLe
+import okio.internal.commonWriteShort
+import okio.internal.commonWriteShortLe
+import okio.internal.commonWriteUtf8
+import okio.internal.commonWriteUtf8CodePoint
+
+internal actual class RealBufferedSink actual constructor(
+ actual val sink: Sink
+) : BufferedSink {
+ actual var closed: Boolean = false
+ override val buffer = Buffer()
+
+ override fun write(source: Buffer, byteCount: Long) = commonWrite(source, byteCount)
+ override fun write(byteString: ByteString) = commonWrite(byteString)
+ override fun write(byteString: ByteString, offset: Int, byteCount: Int) =
+ commonWrite(byteString, offset, byteCount)
+ override fun writeUtf8(string: String) = commonWriteUtf8(string)
+ override fun writeUtf8(string: String, beginIndex: Int, endIndex: Int) =
+ commonWriteUtf8(string, beginIndex, endIndex)
+
+ override fun writeUtf8CodePoint(codePoint: Int) = commonWriteUtf8CodePoint(codePoint)
+ override fun write(source: ByteArray) = commonWrite(source)
+ override fun write(source: ByteArray, offset: Int, byteCount: Int) =
+ commonWrite(source, offset, byteCount)
+
+ override fun writeAll(source: Source) = commonWriteAll(source)
+ override fun write(source: Source, byteCount: Long): BufferedSink = commonWrite(source, byteCount)
+ override fun writeByte(b: Int) = commonWriteByte(b)
+ override fun writeShort(s: Int) = commonWriteShort(s)
+ override fun writeShortLe(s: Int) = commonWriteShortLe(s)
+ override fun writeInt(i: Int) = commonWriteInt(i)
+ override fun writeIntLe(i: Int) = commonWriteIntLe(i)
+ override fun writeLong(v: Long) = commonWriteLong(v)
+ override fun writeLongLe(v: Long) = commonWriteLongLe(v)
+ override fun writeDecimalLong(v: Long) = commonWriteDecimalLong(v)
+ override fun writeHexadecimalUnsignedLong(v: Long) = commonWriteHexadecimalUnsignedLong(v)
+ override fun emitCompleteSegments() = commonEmitCompleteSegments()
+ override fun emit() = commonEmit()
+ override fun flush() = commonFlush()
+ override fun close() = commonClose()
+ override fun timeout() = commonTimeout()
+ override fun toString() = commonToString()
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/RealBufferedSource.kt b/okio/src/nonJvmMain/kotlin/okio/RealBufferedSource.kt
new file mode 100644
index 00000000..d6f4b942
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/RealBufferedSource.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.commonClose
+import okio.internal.commonExhausted
+import okio.internal.commonIndexOf
+import okio.internal.commonIndexOfElement
+import okio.internal.commonPeek
+import okio.internal.commonRangeEquals
+import okio.internal.commonRead
+import okio.internal.commonReadAll
+import okio.internal.commonReadByte
+import okio.internal.commonReadByteArray
+import okio.internal.commonReadByteString
+import okio.internal.commonReadDecimalLong
+import okio.internal.commonReadFully
+import okio.internal.commonReadHexadecimalUnsignedLong
+import okio.internal.commonReadInt
+import okio.internal.commonReadIntLe
+import okio.internal.commonReadLong
+import okio.internal.commonReadLongLe
+import okio.internal.commonReadShort
+import okio.internal.commonReadShortLe
+import okio.internal.commonReadUtf8
+import okio.internal.commonReadUtf8CodePoint
+import okio.internal.commonReadUtf8Line
+import okio.internal.commonReadUtf8LineStrict
+import okio.internal.commonRequest
+import okio.internal.commonRequire
+import okio.internal.commonSelect
+import okio.internal.commonSkip
+import okio.internal.commonTimeout
+import okio.internal.commonToString
+
+internal actual class RealBufferedSource actual constructor(
+ actual val source: Source
+) : BufferedSource {
+ actual var closed: Boolean = false
+ override val buffer: Buffer = Buffer()
+
+ override fun read(sink: Buffer, byteCount: Long): Long = commonRead(sink, byteCount)
+ override fun exhausted(): Boolean = commonExhausted()
+ override fun require(byteCount: Long): Unit = commonRequire(byteCount)
+ override fun request(byteCount: Long): Boolean = commonRequest(byteCount)
+ override fun readByte(): Byte = commonReadByte()
+ override fun readByteString(): ByteString = commonReadByteString()
+ override fun readByteString(byteCount: Long): ByteString = commonReadByteString(byteCount)
+ override fun select(options: Options): Int = commonSelect(options)
+ override fun readByteArray(): ByteArray = commonReadByteArray()
+ override fun readByteArray(byteCount: Long): ByteArray = commonReadByteArray(byteCount)
+ override fun read(sink: ByteArray): Int = read(sink, 0, sink.size)
+ override fun readFully(sink: ByteArray): Unit = commonReadFully(sink)
+ override fun read(sink: ByteArray, offset: Int, byteCount: Int): Int =
+ commonRead(sink, offset, byteCount)
+
+ override fun readFully(sink: Buffer, byteCount: Long): Unit = commonReadFully(sink, byteCount)
+ override fun readAll(sink: Sink): Long = commonReadAll(sink)
+ override fun readUtf8(): String = commonReadUtf8()
+ override fun readUtf8(byteCount: Long): String = commonReadUtf8(byteCount)
+ override fun readUtf8Line(): String? = commonReadUtf8Line()
+ override fun readUtf8LineStrict() = readUtf8LineStrict(Long.MAX_VALUE)
+ override fun readUtf8LineStrict(limit: Long): String = commonReadUtf8LineStrict(limit)
+ override fun readUtf8CodePoint(): Int = commonReadUtf8CodePoint()
+ override fun readShort(): Short = commonReadShort()
+ override fun readShortLe(): Short = commonReadShortLe()
+ override fun readInt(): Int = commonReadInt()
+ override fun readIntLe(): Int = commonReadIntLe()
+ override fun readLong(): Long = commonReadLong()
+ override fun readLongLe(): Long = commonReadLongLe()
+ override fun readDecimalLong(): Long = commonReadDecimalLong()
+ override fun readHexadecimalUnsignedLong(): Long = commonReadHexadecimalUnsignedLong()
+ override fun skip(byteCount: Long): Unit = commonSkip(byteCount)
+ override fun indexOf(b: Byte): Long = indexOf(b, 0L, Long.MAX_VALUE)
+ override fun indexOf(b: Byte, fromIndex: Long): Long = indexOf(b, fromIndex, Long.MAX_VALUE)
+ override fun indexOf(b: Byte, fromIndex: Long, toIndex: Long): Long =
+ commonIndexOf(b, fromIndex, toIndex)
+
+ override fun indexOf(bytes: ByteString): Long = indexOf(bytes, 0L)
+ override fun indexOf(bytes: ByteString, fromIndex: Long): Long = commonIndexOf(bytes, fromIndex)
+ override fun indexOfElement(targetBytes: ByteString): Long = indexOfElement(targetBytes, 0L)
+ override fun indexOfElement(targetBytes: ByteString, fromIndex: Long): Long =
+ commonIndexOfElement(targetBytes, fromIndex)
+
+ override fun rangeEquals(offset: Long, bytes: ByteString) = rangeEquals(
+ offset, bytes, 0,
+ bytes.size
+ )
+
+ override fun rangeEquals(
+ offset: Long,
+ bytes: ByteString,
+ bytesOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, bytes, bytesOffset, byteCount)
+
+ override fun peek(): BufferedSource = commonPeek()
+ override fun close(): Unit = commonClose()
+ override fun timeout(): Timeout = commonTimeout()
+ override fun toString(): String = commonToString()
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/SegmentPool.kt b/okio/src/nonJvmMain/kotlin/okio/SegmentPool.kt
new file mode 100644
index 00000000..50e70991
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/SegmentPool.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+internal actual object SegmentPool {
+ actual val MAX_SIZE: Int = 0
+
+ actual val byteCount: Int = 0
+
+ actual fun take(): Segment = Segment()
+
+ actual fun recycle(segment: Segment) {
+ }
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/SegmentedByteString.kt b/okio/src/nonJvmMain/kotlin/okio/SegmentedByteString.kt
new file mode 100644
index 00000000..fe718901
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/SegmentedByteString.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+import okio.internal.HashFunction
+import okio.internal.commonEquals
+import okio.internal.commonGetSize
+import okio.internal.commonHashCode
+import okio.internal.commonInternalGet
+import okio.internal.commonRangeEquals
+import okio.internal.commonSubstring
+import okio.internal.commonToByteArray
+import okio.internal.commonWrite
+import okio.internal.forEachSegment
+
+internal actual class SegmentedByteString internal actual constructor(
+ internal actual val segments: Array<ByteArray>,
+ internal actual val directory: IntArray
+) : ByteString(EMPTY.data) {
+
+ override fun base64() = toByteString().base64()
+
+ override fun hex() = toByteString().hex()
+
+ override fun toAsciiLowercase() = toByteString().toAsciiLowercase()
+
+ override fun toAsciiUppercase() = toByteString().toAsciiUppercase()
+
+ override fun base64Url() = toByteString().base64Url()
+
+ override fun substring(beginIndex: Int, endIndex: Int): ByteString =
+ commonSubstring(beginIndex, endIndex)
+
+ override fun internalGet(pos: Int): Byte = commonInternalGet(pos)
+
+ override fun getSize() = commonGetSize()
+
+ override fun toByteArray(): ByteArray = commonToByteArray()
+
+ override fun write(buffer: Buffer, offset: Int, byteCount: Int): Unit =
+ commonWrite(buffer, offset, byteCount)
+
+ override fun rangeEquals(
+ offset: Int,
+ other: ByteString,
+ otherOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount)
+
+ override fun rangeEquals(
+ offset: Int,
+ other: ByteArray,
+ otherOffset: Int,
+ byteCount: Int
+ ): Boolean = commonRangeEquals(offset, other, otherOffset, byteCount)
+
+ override fun indexOf(other: ByteArray, fromIndex: Int) = toByteString().indexOf(other, fromIndex)
+
+ override fun lastIndexOf(other: ByteArray, fromIndex: Int) = toByteString().lastIndexOf(
+ other,
+ fromIndex
+ )
+
+ override fun digest(hashFunction: HashFunction): ByteString {
+ forEachSegment { data, offset, byteCount ->
+ hashFunction.update(data, offset, byteCount)
+ }
+ val digestBytes = hashFunction.digest()
+ return ByteString(digestBytes)
+ }
+
+ /** Returns a copy as a non-segmented byte string. */
+ private fun toByteString() = ByteString(toByteArray())
+
+ override fun internalArray() = toByteArray()
+
+ override fun equals(other: Any?): Boolean = commonEquals(other)
+
+ override fun hashCode(): Int = commonHashCode()
+
+ override fun toString() = toByteString().toString()
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/Sink.kt b/okio/src/nonJvmMain/kotlin/okio/Sink.kt
new file mode 100644
index 00000000..d1146472
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/Sink.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+actual interface Sink : Closeable {
+ @Throws(IOException::class)
+ actual fun write(source: Buffer, byteCount: Long)
+
+ @Throws(IOException::class)
+ actual fun flush()
+
+ actual fun timeout(): Timeout
+
+ @Throws(IOException::class)
+ actual override fun close()
+}
diff --git a/okio/src/nonJvmMain/kotlin/okio/Timeout.kt b/okio/src/nonJvmMain/kotlin/okio/Timeout.kt
new file mode 100644
index 00000000..d66a0274
--- /dev/null
+++ b/okio/src/nonJvmMain/kotlin/okio/Timeout.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2019 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+actual open class Timeout {
+ actual companion object {
+ actual val NONE = Timeout()
+ }
+}
diff --git a/samples/build.gradle b/samples/build.gradle
new file mode 100644
index 00000000..efaef19e
--- /dev/null
+++ b/samples/build.gradle
@@ -0,0 +1,23 @@
+apply plugin: 'org.jetbrains.kotlin.multiplatform'
+apply plugin: 'application'
+
+mainClassName = System.getProperty("mainClass")
+
+kotlin {
+ jvm {
+ withJava()
+ }
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation project(':okio')
+ }
+ }
+ jvmTest {
+ dependencies {
+ implementation deps.test.junit
+ implementation deps.test.assertj
+ }
+ }
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java b/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java
new file mode 100644
index 00000000..a505f6dc
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/BitmapEncoder.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.File;
+import java.io.IOException;
+import okio.BufferedSink;
+import okio.Okio;
+
+public final class BitmapEncoder {
+ static final class Bitmap {
+ private final int[][] pixels;
+
+ Bitmap(int[][] pixels) {
+ this.pixels = pixels;
+ }
+
+ int width() {
+ return pixels[0].length;
+ }
+
+ int height() {
+ return pixels.length;
+ }
+
+ int red(int x, int y) {
+ return (pixels[y][x] & 0xff0000) >> 16;
+ }
+
+ int green(int x, int y) {
+ return (pixels[y][x] & 0xff00) >> 8;
+ }
+
+ int blue(int x, int y) {
+ return (pixels[y][x] & 0xff);
+ }
+ }
+
+ /**
+ * Returns a bitmap that lights up red subpixels at the bottom, green subpixels on the right, and
+ * blue subpixels in bottom-right.
+ */
+ Bitmap generateGradient() {
+ int[][] pixels = new int[1080][1920];
+ for (int y = 0; y < 1080; y++) {
+ for (int x = 0; x < 1920; x++) {
+ int r = (int) (y / 1080f * 255);
+ int g = (int) (x / 1920f * 255);
+ int b = (int) ((Math.hypot(x, y) / Math.hypot(1080, 1920)) * 255);
+ pixels[y][x] = r << 16 | g << 8 | b;
+ }
+ }
+ return new Bitmap(pixels);
+ }
+
+ void encode(Bitmap bitmap, File file) throws IOException {
+ try (BufferedSink sink = Okio.buffer(Okio.sink(file))) {
+ encode(bitmap, sink);
+ }
+ }
+
+ /** https://en.wikipedia.org/wiki/BMP_file_format */
+ void encode(Bitmap bitmap, BufferedSink sink) throws IOException {
+ int height = bitmap.height();
+ int width = bitmap.width();
+
+ int bytesPerPixel = 3;
+ int rowByteCountWithoutPadding = (bytesPerPixel * width);
+ int rowByteCount = ((rowByteCountWithoutPadding + 3) / 4) * 4;
+ int pixelDataSize = rowByteCount * height;
+ int bmpHeaderSize = 14;
+ int dibHeaderSize = 40;
+
+ // BMP Header
+ sink.writeUtf8("BM"); // ID.
+ sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize); // File size.
+ sink.writeShortLe(0); // Unused.
+ sink.writeShortLe(0); // Unused.
+ sink.writeIntLe(bmpHeaderSize + dibHeaderSize); // Offset of pixel data.
+
+ // DIB Header
+ sink.writeIntLe(dibHeaderSize);
+ sink.writeIntLe(width);
+ sink.writeIntLe(height);
+ sink.writeShortLe(1); // Color plane count.
+ sink.writeShortLe(bytesPerPixel * Byte.SIZE);
+ sink.writeIntLe(0); // No compression.
+ sink.writeIntLe(16); // Size of bitmap data including padding.
+ sink.writeIntLe(2835); // Horizontal print resolution in pixels/meter. (72 dpi).
+ sink.writeIntLe(2835); // Vertical print resolution in pixels/meter. (72 dpi).
+ sink.writeIntLe(0); // Palette color count.
+ sink.writeIntLe(0); // 0 important colors.
+
+ // Pixel data.
+ for (int y = height - 1; y >= 0; y--) {
+ for (int x = 0; x < width; x++) {
+ sink.writeByte(bitmap.blue(x, y));
+ sink.writeByte(bitmap.green(x, y));
+ sink.writeByte(bitmap.red(x, y));
+ }
+
+ // Padding for 4-byte alignment.
+ for (int p = rowByteCountWithoutPadding; p < rowByteCount; p++) {
+ sink.writeByte(0);
+ }
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ BitmapEncoder encoder = new BitmapEncoder();
+ Bitmap bitmap = encoder.generateGradient();
+ encoder.encode(bitmap, new File("gradient.bmp"));
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/ByteChannelSink.java b/samples/src/jvmMain/java/okio/samples/ByteChannelSink.java
new file mode 100644
index 00000000..ba4a9af3
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/ByteChannelSink.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.WritableByteChannel;
+import okio.Buffer;
+import okio.Sink;
+import okio.Timeout;
+
+/**
+ * Creates a Sink around a WritableByteChannel and efficiently writes data using an UnsafeCursor.
+ *
+ * <p>This is a basic example showing another use for the UnsafeCursor. Using the
+ * {@link ByteBuffer#wrap(byte[], int, int) ByteBuffer.wrap()} along with access to Buffer segments,
+ * a WritableByteChannel can be given direct access to Buffer data without having to copy the data.
+ */
+final class ByteChannelSink implements Sink {
+ private final WritableByteChannel channel;
+ private final Timeout timeout;
+
+ private final Buffer.UnsafeCursor cursor = new Buffer.UnsafeCursor();
+
+ ByteChannelSink(WritableByteChannel channel, Timeout timeout) {
+ this.channel = channel;
+ this.timeout = timeout;
+ }
+
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ if (!channel.isOpen()) throw new IllegalStateException("closed");
+ if (byteCount == 0) return;
+
+ long remaining = byteCount;
+ while (remaining > 0) {
+ timeout.throwIfReached();
+
+ try (Buffer.UnsafeCursor ignored = source.readUnsafe(cursor)) {
+ cursor.seek(0);
+ int length = (int) Math.min(cursor.end - cursor.start, remaining);
+ int written = channel.write(ByteBuffer.wrap(cursor.data, cursor.start, length));
+ remaining -= written;
+ source.skip(written);
+ }
+ }
+ }
+
+ @Override public void flush() {}
+
+ @Override public Timeout timeout() {
+ return timeout;
+ }
+
+ @Override public void close() throws IOException {
+ channel.close();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/ByteChannelSource.java b/samples/src/jvmMain/java/okio/samples/ByteChannelSource.java
new file mode 100644
index 00000000..f1f28759
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/ByteChannelSource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ReadableByteChannel;
+import okio.Buffer;
+import okio.Source;
+import okio.Timeout;
+
+/**
+ * Creates a Source around a ReadableByteChannel and efficiently reads data using an UnsafeCursor.
+ *
+ * <p>This is a basic example showing another use for the UnsafeCursor. Using the
+ * {@link ByteBuffer#wrap(byte[], int, int) ByteBuffer.wrap()} along with access to Buffer segments,
+ * a ReadableByteChannel can be given direct access to Buffer data without having to copy the data.
+ */
+final class ByteChannelSource implements Source {
+ private final ReadableByteChannel channel;
+ private final Timeout timeout;
+
+ private final Buffer.UnsafeCursor cursor = new Buffer.UnsafeCursor();
+
+ ByteChannelSource(ReadableByteChannel channel, Timeout timeout) {
+ this.channel = channel;
+ this.timeout = timeout;
+ }
+
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ if (!channel.isOpen()) throw new IllegalStateException("closed");
+
+ try (Buffer.UnsafeCursor ignored = sink.readAndWriteUnsafe(cursor)) {
+ timeout.throwIfReached();
+ long oldSize = sink.size();
+ int length = (int) Math.min(8192, byteCount);
+
+ cursor.expandBuffer(length);
+ int read = channel.read(ByteBuffer.wrap(cursor.data, cursor.start, length));
+ if (read == -1) {
+ cursor.resizeBuffer(oldSize);
+ return -1;
+ } else {
+ cursor.resizeBuffer(oldSize + read);
+ return read;
+ }
+ }
+ }
+
+ @Override public Timeout timeout() {
+ return timeout;
+ }
+
+ @Override public void close() throws IOException {
+ channel.close();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/ExploreCharsets.java b/samples/src/jvmMain/java/okio/samples/ExploreCharsets.java
new file mode 100644
index 00000000..3b20295f
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/ExploreCharsets.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import okio.ByteString;
+import okio.Utf8;
+
+public final class ExploreCharsets {
+ public void run() throws Exception {
+ dumpStringData("Café \uD83C\uDF69"); // NFC: é is one code point.
+ dumpStringData("Café \uD83C\uDF69"); // NFD: e is one code point, its accent is another.
+ }
+
+ public void dumpStringData(String s) throws IOException {
+ System.out.println(" " + s);
+ System.out.println(" String.length: " + s.length());
+ System.out.println("String.codePointCount: " + s.codePointCount(0, s.length()));
+ System.out.println(" Utf8.size: " + Utf8.size(s));
+ System.out.println(" UTF-8 bytes: " + ByteString.encodeUtf8(s).hex());
+ System.out.println();
+ }
+
+ public static void main(String... args) throws Exception {
+ new ExploreCharsets().run();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/FileChannelSink.java b/samples/src/jvmMain/java/okio/samples/FileChannelSink.java
new file mode 100644
index 00000000..b810a328
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/FileChannelSink.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import okio.Buffer;
+import okio.Sink;
+import okio.Timeout;
+
+/**
+ * Special Sink for a FileChannel to take advantage of the
+ * {@link FileChannel#transferFrom(ReadableByteChannel, long, long) transfer} method available.
+ */
+final class FileChannelSink implements Sink {
+ private final FileChannel channel;
+ private final Timeout timeout;
+
+ private long position;
+
+ FileChannelSink(FileChannel channel, Timeout timeout) throws IOException {
+ this.channel = channel;
+ this.timeout = timeout;
+
+ this.position = channel.position();
+ }
+
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ if (!channel.isOpen()) throw new IllegalStateException("closed");
+ if (byteCount == 0) return;
+
+ long remaining = byteCount;
+ while (remaining > 0) {
+ long written = channel.transferFrom(source, position, remaining);
+ position += written;
+ remaining -= written;
+ }
+ }
+
+ @Override public void flush() throws IOException {
+ // Cannot alter meta data through this Sink
+ channel.force(false);
+ }
+
+ @Override public Timeout timeout() {
+ return timeout;
+ }
+
+ @Override public void close() throws IOException {
+ channel.close();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/FileChannelSource.java b/samples/src/jvmMain/java/okio/samples/FileChannelSource.java
new file mode 100644
index 00000000..db5ec935
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/FileChannelSource.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.WritableByteChannel;
+import okio.Buffer;
+import okio.Source;
+import okio.Timeout;
+
+/**
+ * Special Source for a FileChannel to take advantage of the
+ * {@link FileChannel#transferTo(long, long, WritableByteChannel) transfer} method available.
+ */
+final class FileChannelSource implements Source {
+ private final FileChannel channel;
+ private final Timeout timeout;
+
+ private long position;
+
+ FileChannelSource(FileChannel channel, Timeout timeout) throws IOException {
+ this.channel = channel;
+ this.timeout = timeout;
+
+ this.position = channel.position();
+ }
+
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ if (!channel.isOpen()) throw new IllegalStateException("closed");
+ if (position == channel.size()) return -1L;
+
+ long read = channel.transferTo(position, byteCount, sink);
+ position += read;
+ return read;
+ }
+
+ @Override public Timeout timeout() {
+ return timeout;
+ }
+
+ @Override public void close() throws IOException {
+ channel.close();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/GoldenValue.java b/samples/src/jvmMain/java/okio/samples/GoldenValue.java
new file mode 100644
index 00000000..aeeaa0ff
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/GoldenValue.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import okio.Buffer;
+import okio.ByteString;
+
+public final class GoldenValue {
+ public void run() throws Exception {
+ Point point = new Point(8.0, 15.0);
+ ByteString pointBytes = serialize(point);
+ System.out.println(pointBytes.base64());
+
+ ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ"
+ + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA"
+ + "AAAAAAA");
+ Point decoded = (Point) deserialize(goldenBytes);
+ assertEquals(point, decoded);
+ }
+
+ private ByteString serialize(Object o) throws IOException {
+ Buffer buffer = new Buffer();
+ try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) {
+ objectOut.writeObject(o);
+ }
+ return buffer.readByteString();
+ }
+
+ private Object deserialize(ByteString byteString) throws IOException, ClassNotFoundException {
+ Buffer buffer = new Buffer();
+ buffer.write(byteString);
+ try (ObjectInputStream objectIn = new ObjectInputStream(buffer.inputStream())) {
+ Object result = objectIn.readObject();
+ if (objectIn.read() != -1) throw new IOException("Unconsumed bytes in stream");
+ return result;
+ }
+ }
+
+ static final class Point implements Serializable {
+ double x;
+ double y;
+
+ Point(double x, double y) {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ private void assertEquals(Point a, Point b) {
+ if (a.x != b.x || a.y != b.y) throw new AssertionError();
+ }
+
+ public static void main(String... args) throws Exception {
+ new GoldenValue().run();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/Hashing.java b/samples/src/jvmMain/java/okio/samples/Hashing.java
new file mode 100644
index 00000000..0f2b4474
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/Hashing.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.File;
+import java.io.IOException;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.HashingSink;
+import okio.HashingSource;
+import okio.Okio;
+import okio.Source;
+
+public final class Hashing {
+ public void run() throws Exception {
+ File file = new File("../README.md");
+
+ System.out.println("ByteString");
+ ByteString byteString = readByteString(file);
+ System.out.println(" md5: " + byteString.md5().hex());
+ System.out.println(" sha1: " + byteString.sha1().hex());
+ System.out.println(" sha256: " + byteString.sha256().hex());
+ System.out.println(" sha512: " + byteString.sha512().hex());
+ System.out.println();
+
+ System.out.println("Buffer");
+ Buffer buffer = readBuffer(file);
+ System.out.println(" md5: " + buffer.md5().hex());
+ System.out.println(" sha1: " + buffer.sha1().hex());
+ System.out.println(" sha256: " + buffer.sha256().hex());
+ System.out.println(" sha512: " + buffer.sha512().hex());
+ System.out.println();
+
+ System.out.println("HashingSource");
+ try (HashingSource hashingSource = HashingSource.sha256(Okio.source(file));
+ BufferedSource source = Okio.buffer(hashingSource)) {
+ source.readAll(Okio.blackhole());
+ System.out.println(" sha256: " + hashingSource.hash().hex());
+ }
+ System.out.println();
+
+ System.out.println("HashingSink");
+ try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());
+ BufferedSink sink = Okio.buffer(hashingSink);
+ Source source = Okio.source(file)) {
+ sink.writeAll(source);
+ sink.close(); // Emit anything buffered.
+ System.out.println(" sha256: " + hashingSink.hash().hex());
+ }
+ System.out.println();
+
+ System.out.println("HMAC");
+ ByteString secret = ByteString.decodeHex("7065616e7574627574746572");
+ System.out.println("hmacSha256: " + byteString.hmacSha256(secret).hex());
+ System.out.println();
+ }
+
+ public ByteString readByteString(File file) throws IOException {
+ try (BufferedSource source = Okio.buffer(Okio.source(file))) {
+ return source.readByteString();
+ }
+ }
+
+ public Buffer readBuffer(File file) throws IOException {
+ try (Source source = Okio.source(file)) {
+ Buffer buffer = new Buffer();
+ buffer.writeAll(source);
+ return buffer;
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ new Hashing().run();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/Interceptors.java b/samples/src/jvmMain/java/okio/samples/Interceptors.java
new file mode 100644
index 00000000..85cbbba5
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/Interceptors.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import java.util.Random;
+import okio.Buffer;
+import okio.ForwardingSink;
+import okio.ForwardingSource;
+import okio.Sink;
+import okio.Source;
+
+/**
+ * Demonstrates use of the {@link Buffer.UnsafeCursor} class. While other
+ * samples might demonstrate real use cases, this sample hopes to show the
+ * basics of using an {@link Buffer.UnsafeCursor}:
+ * <ul>
+ * <li>Efficient reuse of a single cursor instance.</li>
+ * <li>Guaranteed release of an attached cursor.</li>
+ * <li>Safe traversal of the data in a Buffer.</li>
+ * </ul>
+ *
+ * <p>This sample implements a
+ * <a href="https://en.wikipedia.org/wiki/Cipher_disk">circular cipher</a> by
+ * creating a Source which will intercept all bytes written to the wire and
+ * decrease their value by a specific amount. Then create a Sink which will
+ * intercept all bytes read from the wire and increase their value by that same
+ * specific amount. This creates an incredibly insecure way of encrypting data
+ * written to the wire but demonstrates the power of the
+ * {@link Buffer.UnsafeCursor} class for efficient operations on the bytes
+ * being written and read.
+ */
+public final class Interceptors {
+ public void run() throws Exception {
+ final byte cipher = (byte) (new Random().nextInt(256) - 128);
+ System.out.println("Cipher : " + cipher);
+
+ Buffer wire = new Buffer();
+
+ // Create a Sink which will intercept and negatively rotate each byte by `cipher`
+ Sink sink = new InterceptingSink(wire) {
+ @Override
+ protected void intercept(byte[] data, int offset, int length) {
+ for (int i = offset, end = offset + length; i < end; i++) {
+ data[i] -= cipher;
+ }
+ }
+ };
+
+ // Create a Source which will intercept and positively rotate each byte by `cipher`
+ Source source = new InterceptingSource(wire) {
+ @Override
+ protected void intercept(byte[] data, int offset, int length) {
+ for (int i = offset, end = offset + length; i < end; i++) {
+ data[i] += cipher;
+ }
+ }
+ };
+
+ Buffer transmit = new Buffer();
+ transmit.writeUtf8("This is not really a secure message");
+ System.out.println("Transmit : " + transmit);
+
+ sink.write(transmit, transmit.size());
+ System.out.println("Wire : " + wire);
+
+ Buffer receive = new Buffer();
+ source.read(receive, Long.MAX_VALUE);
+ System.out.println("Receive : " + receive);
+ }
+
+ abstract class InterceptingSource extends ForwardingSource {
+
+ private final Buffer.UnsafeCursor cursor = new Buffer.UnsafeCursor();
+
+ InterceptingSource(Source source) {
+ super(source);
+ }
+
+ @Override
+ public long read(Buffer sink, long byteCount) throws IOException {
+ if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+ if (byteCount == 0) return 0;
+
+ long result = super.read(sink, byteCount);
+ if (result == -1L) return result;
+
+ sink.readUnsafe(cursor);
+ try {
+ long remaining = result;
+ for (int length = cursor.seek(sink.size() - result);
+ remaining > 0 && length > 0;
+ length = cursor.next()) {
+ int toIntercept = (int) Math.min(length, remaining);
+ intercept(cursor.data, cursor.start, toIntercept);
+ remaining -= toIntercept;
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return result;
+ }
+
+ protected abstract void intercept(byte[] data, int offset, int length) throws IOException;
+ }
+
+
+ abstract class InterceptingSink extends ForwardingSink {
+
+ private final Buffer.UnsafeCursor cursor = new Buffer.UnsafeCursor();
+
+ InterceptingSink(Sink delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public void write(Buffer source, long byteCount) throws IOException {
+ if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+ if (source.size() < byteCount) {
+ throw new IllegalArgumentException("size=" + source.size() + " byteCount=" + byteCount);
+ }
+ if (byteCount == 0) return;
+
+ source.readUnsafe(cursor);
+ try {
+ long remaining = byteCount;
+ for (int length = cursor.seek(0);
+ remaining > 0 && length > 0;
+ length = cursor.next()) {
+ int toIntercept = (int) Math.min(length, remaining);
+ intercept(cursor.data, cursor.start, toIntercept);
+ remaining -= toIntercept;
+ }
+ } finally {
+ cursor.close();
+ }
+
+ super.write(source, byteCount);
+ }
+
+ protected abstract void intercept(byte[] data, int offset, int length) throws IOException;
+ }
+
+ public static void main(String... args) throws Exception {
+ new Interceptors().run();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/Randoms.java b/samples/src/jvmMain/java/okio/samples/Randoms.java
new file mode 100644
index 00000000..631ebc98
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/Randoms.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import java.util.Random;
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+import okio.Timeout;
+
+public final class Randoms {
+ public void run() throws IOException, InterruptedException {
+ Random random = new Random(3782615686L);
+ BufferedSource source = Okio.buffer(new RandomSource(random, 5));
+ System.out.println(source.readUtf8());
+ }
+
+ static final class RandomSource implements Source {
+ private final Random random;
+ private long bytesLeft;
+
+ RandomSource(Random random, long bytesLeft) {
+ this.random = random;
+ this.bytesLeft = bytesLeft;
+ }
+
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ if (bytesLeft == -1L) throw new IllegalStateException("closed");
+ if (bytesLeft == 0L) return -1L;
+ if (byteCount > Integer.MAX_VALUE) byteCount = Integer.MAX_VALUE;
+ if (byteCount > bytesLeft) byteCount = bytesLeft;
+
+ // Random is most efficient when computing 32 bits of randomness. Start with that.
+ int ints = (int) (byteCount / 4);
+ for (int i = 0; i < ints; i++) {
+ sink.writeInt(random.nextInt());
+ }
+
+ // If we need 1, 2, or 3 bytes more, keep going. We'll discard 24, 16 or 8 random bits!
+ int bytes = (int) (byteCount - ints * 4);
+ if (bytes > 0) {
+ int bits = random.nextInt();
+ for (int i = 0; i < bytes; i++) {
+ sink.writeByte(bits & 0xff);
+ bits >>>= 8;
+ }
+ }
+
+ bytesLeft -= byteCount;
+ return byteCount;
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ @Override public void close() throws IOException {
+ bytesLeft = -1L;
+ }
+ }
+
+ public static void main(String... args) throws Exception {
+ new Randoms().run();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java b/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java
new file mode 100644
index 00000000..b195a513
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/ReadFileLineByLine.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.File;
+import java.io.IOException;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+
+public final class ReadFileLineByLine {
+ public void run() throws Exception {
+ readLines(new File("../README.md"));
+ }
+
+ public void readLines(File file) throws IOException {
+ try (Source fileSource = Okio.source(file);
+ BufferedSource bufferedFileSource = Okio.buffer(fileSource)) {
+
+ while (true) {
+ String line = bufferedFileSource.readUtf8Line();
+ if (line == null) break;
+
+ if (line.contains("square")) {
+ System.out.println(line);
+ }
+ }
+
+ }
+ }
+
+ public static void main(String... args) throws Exception {
+ new ReadFileLineByLine().run();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/SocksProxyServer.java b/samples/src/jvmMain/java/okio/samples/SocksProxyServer.java
new file mode 100644
index 00000000..d770d827
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/SocksProxyServer.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+
+/**
+ * A partial implementation of SOCKS Protocol Version 5.
+ * See <a href="https://www.ietf.org/rfc/rfc1928.txt">RFC 1928</a>.
+ */
+public final class SocksProxyServer {
+ private static final int VERSION_5 = 5;
+ private static final int METHOD_NO_AUTHENTICATION_REQUIRED = 0;
+ private static final int ADDRESS_TYPE_IPV4 = 1;
+ private static final int ADDRESS_TYPE_DOMAIN_NAME = 3;
+ private static final int COMMAND_CONNECT = 1;
+ private static final int REPLY_SUCCEEDED = 0;
+
+ private final ExecutorService executor = Executors.newCachedThreadPool();
+ private ServerSocket serverSocket;
+ private final Set<Socket> openSockets =
+ Collections.newSetFromMap(new ConcurrentHashMap<>());
+
+ public void start() throws IOException {
+ serverSocket = new ServerSocket(0);
+ executor.execute(this::acceptSockets);
+ }
+
+ public void shutdown() throws IOException {
+ serverSocket.close();
+ executor.shutdown();
+ }
+
+ public Proxy proxy() {
+ return new Proxy(Proxy.Type.SOCKS,
+ InetSocketAddress.createUnresolved("localhost", serverSocket.getLocalPort()));
+ }
+
+ private void acceptSockets() {
+ try {
+ while (true) {
+ final Socket from = serverSocket.accept();
+ openSockets.add(from);
+ executor.execute(() -> handleSocket(from));
+ }
+ } catch (IOException e) {
+ System.out.println("shutting down: " + e);
+ } finally {
+ for (Socket socket : openSockets) {
+ closeQuietly(socket);
+ }
+ }
+ }
+
+ private void handleSocket(final Socket fromSocket) {
+ try {
+ final BufferedSource fromSource = Okio.buffer(Okio.source(fromSocket));
+ final BufferedSink fromSink = Okio.buffer(Okio.sink(fromSocket));
+
+ // Read the hello.
+ int socksVersion = fromSource.readByte() & 0xff;
+ if (socksVersion != VERSION_5) throw new ProtocolException();
+ int methodCount = fromSource.readByte() & 0xff;
+ boolean foundSupportedMethod = false;
+ for (int i = 0; i < methodCount; i++) {
+ int method = fromSource.readByte() & 0xff;
+ foundSupportedMethod |= method == METHOD_NO_AUTHENTICATION_REQUIRED;
+ }
+ if (!foundSupportedMethod) throw new ProtocolException();
+
+ // Respond to hello.
+ fromSink.writeByte(VERSION_5)
+ .writeByte(METHOD_NO_AUTHENTICATION_REQUIRED)
+ .emit();
+
+ // Read a command.
+ int version = fromSource.readByte() & 0xff;
+ int command = fromSource.readByte() & 0xff;
+ int reserved = fromSource.readByte() & 0xff;
+ if (version != VERSION_5 || command != COMMAND_CONNECT || reserved != 0) {
+ throw new ProtocolException();
+ }
+
+ // Read an address.
+ int addressType = fromSource.readByte() & 0xff;
+ InetAddress inetAddress;
+ if (addressType == ADDRESS_TYPE_IPV4) {
+ inetAddress = InetAddress.getByAddress(fromSource.readByteArray(4L));
+ } else if (addressType == ADDRESS_TYPE_DOMAIN_NAME){
+ int domainNameLength = fromSource.readByte() & 0xff;
+ inetAddress = InetAddress.getByName(fromSource.readUtf8(domainNameLength));
+ } else {
+ throw new ProtocolException();
+ }
+ int port = fromSource.readShort() & 0xffff;
+
+ // Connect to the caller's specified host.
+ final Socket toSocket = new Socket(inetAddress, port);
+ openSockets.add(toSocket);
+ byte[] localAddress = toSocket.getLocalAddress().getAddress();
+ if (localAddress.length != 4) throw new ProtocolException();
+
+ // Write the reply.
+ fromSink.writeByte(VERSION_5)
+ .writeByte(REPLY_SUCCEEDED)
+ .writeByte(0)
+ .writeByte(ADDRESS_TYPE_IPV4)
+ .write(localAddress)
+ .writeShort(toSocket.getLocalPort())
+ .emit();
+
+ // Connect sources to sinks in both directions.
+ final Sink toSink = Okio.sink(toSocket);
+ executor.execute(() -> transfer(fromSocket, fromSource, toSink));
+ final Source toSource = Okio.source(toSocket);
+ executor.execute(() -> transfer(toSocket, toSource, fromSink));
+ } catch (IOException e) {
+ closeQuietly(fromSocket);
+ openSockets.remove(fromSocket);
+ System.out.println("connect failed for " + fromSocket + ": " + e);
+ }
+ }
+
+ /**
+ * Read data from {@code source} and write it to {@code sink}. This doesn't use {@link
+ * BufferedSink#writeAll} because that method doesn't flush aggressively and we need that.
+ */
+ private void transfer(Socket sourceSocket, Source source, Sink sink) {
+ try {
+ Buffer buffer = new Buffer();
+ for (long byteCount; (byteCount = source.read(buffer, 8192L)) != -1; ) {
+ sink.write(buffer, byteCount);
+ sink.flush();
+ }
+ } catch (IOException e) {
+ System.out.println("transfer failed from " + sourceSocket + ": " + e);
+ } finally {
+ closeQuietly(sink);
+ closeQuietly(source);
+ closeQuietly(sourceSocket);
+ openSockets.remove(sourceSocket);
+ }
+ }
+
+ private void closeQuietly(Closeable c) {
+ try {
+ c.close();
+ } catch (IOException ignored) {
+ }
+ }
+
+ public static void main(String[] args) throws IOException {
+ SocksProxyServer proxyServer = new SocksProxyServer();
+ proxyServer.start();
+
+ URL url = new URL("https://publicobject.com/helloworld.txt");
+ URLConnection connection = url.openConnection(proxyServer.proxy());
+ try (BufferedSource source = Okio.buffer(Okio.source(connection.getInputStream()))) {
+ for (String line; (line = source.readUtf8Line()) != null; ) {
+ System.out.println(line);
+ }
+ }
+
+ proxyServer.shutdown();
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/SourceMarker.java b/samples/src/jvmMain/java/okio/samples/SourceMarker.java
new file mode 100644
index 00000000..8deef05e
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/SourceMarker.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.ForwardingSource;
+import okio.Okio;
+import okio.Source;
+
+/**
+ * Builds a buffered source that can rewind to a marked position earlier in the stream.
+ *
+ * <p>Mark potential positions to rewind back to with {@link #mark}; rewind back to these positions
+ * with {@link #reset}. Both operations apply to the position in the {@linkplain #source() buffered
+ * source}; resetting will impact the buffer.
+ *
+ * <p>When marking it is necessary to specify how much data to retain. Once you advance above this
+ * limit, the mark is discarded and resetting is not permitted. This may be used to lookahead a
+ * fixed number of bytes without loading an entire stream into memory. To reset an arbitrary
+ * number of bytes use {@code mark(Long#MAX_VALUE)}.
+ */
+public final class SourceMarker {
+
+ /*
+ * This class wraps the underlying source in a MarkSource to support mark and reset. It creates a
+ * BufferedSource for the caller so that it can track its offsets and manipulate its buffer.
+ */
+
+ /**
+ * The offset into the underlying source. To compute the user's offset start with this and
+ * subtract userBuffer.size().
+ */
+ long offset;
+
+ /** The offset of the earliest mark, or -1 for no mark. */
+ long mark = -1L;
+
+ /** The offset of the latest readLimit, or -1 for no mark. */
+ long limit = -1L;
+
+ boolean closed;
+
+ final MarkSource markSource;
+ final BufferedSource userSource;
+
+ /** A copy of the underlying source's data beginning at {@code mark}. */
+ final Buffer markBuffer;
+
+ /** Just the userSource's buffer. */
+ final Buffer userBuffer;
+
+ public SourceMarker(Source source) {
+ this.markSource = new MarkSource(source);
+ this.markBuffer = new Buffer();
+ this.userSource = Okio.buffer(markSource);
+ this.userBuffer = userSource.getBuffer();
+ }
+
+ public BufferedSource source() {
+ return userSource;
+ }
+
+ /**
+ * Marks the current position in the stream as one to potentially return back to. Returns the
+ * offset of this position. Call {@link #reset(long)} with this position to return to it later. It
+ * is an error to call {@link #reset(long)} after consuming more than {@code readLimit} bytes from
+ * {@linkplain #source() the source}.
+ */
+ public long mark(long readLimit) throws IOException {
+ if (readLimit < 0L) {
+ throw new IllegalArgumentException("readLimit < 0: " + readLimit);
+ }
+
+ if (closed) {
+ throw new IllegalStateException("closed");
+ }
+
+ // Mark the current position in the buffered source.
+ long userOffset = offset - userBuffer.size();
+
+ // If this is a new mark promote userBuffer data into the markBuffer.
+ if (mark == -1L) {
+ markBuffer.writeAll(userBuffer);
+ mark = userOffset;
+ offset = userOffset;
+ }
+
+ // Grow the limit if necessary.
+ long newMarkBufferLimit = userOffset + readLimit;
+ if (newMarkBufferLimit < 0) newMarkBufferLimit = Long.MAX_VALUE; // Long overflow!
+ limit = Math.max(limit, newMarkBufferLimit);
+
+ return userOffset;
+ }
+
+ /** Resets {@linkplain #source() the source} to {@code userOffset}. */
+ public void reset(long userOffset) throws IOException {
+ if (closed) {
+ throw new IllegalStateException("closed");
+ }
+
+ if (userOffset < mark // userOffset is before mark.
+ || userOffset > limit // userOffset is beyond limit.
+ || userOffset > mark + markBuffer.size() // userOffset is in the future.
+ || offset - userBuffer.size() > limit) { // Stream advanced beyond limit.
+ throw new IOException("cannot reset to " + userOffset + ": out of range");
+ }
+
+ // Clear userBuffer to cause data at 'offset' to be returned by the next read.
+ offset = userOffset;
+ userBuffer.clear();
+ }
+
+ final class MarkSource extends ForwardingSource {
+ MarkSource(Source source) {
+ super(source);
+ }
+
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ if (closed) {
+ throw new IllegalStateException("closed");
+ }
+
+ // If there's no mark, go to the underlying source.
+ if (mark == -1L) {
+ long result = super.read(sink, byteCount);
+ if (result == -1L) return -1L;
+ offset += result;
+ return result;
+ }
+
+ // If we can read from markBuffer, do that.
+ if (offset < mark + markBuffer.size()) {
+ long posInBuffer = offset - mark;
+ long result = Math.min(byteCount, markBuffer.size() - posInBuffer);
+ markBuffer.copyTo(sink, posInBuffer, result);
+ offset += result;
+ return result;
+ }
+
+ // If we can write to markBuffer, do that.
+ if (offset < limit) {
+ long byteCountBeforeLimit = limit - (mark + markBuffer.size());
+ long result = super.read(markBuffer, Math.min(byteCount, byteCountBeforeLimit));
+ if (result == -1L) return -1L;
+ markBuffer.copyTo(sink, markBuffer.size() - result, result);
+ offset += result;
+ return result;
+ }
+
+ // Attempt to read past the limit. Data will not be saved.
+ long result = super.read(sink, byteCount);
+ if (result == -1L) return -1L;
+
+ // We read past the limit. Discard marked data.
+ markBuffer.clear();
+ mark = -1L;
+ limit = -1L;
+ return result;
+ }
+
+ @Override public void close() throws IOException {
+ if (closed) return;
+
+ closed = true;
+ markBuffer.clear();
+ super.close();
+ }
+ }
+}
diff --git a/samples/src/jvmMain/java/okio/samples/WriteFile.java b/samples/src/jvmMain/java/okio/samples/WriteFile.java
new file mode 100644
index 00000000..e613abe5
--- /dev/null
+++ b/samples/src/jvmMain/java/okio/samples/WriteFile.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Sink;
+
+public final class WriteFile {
+ public void run() throws Exception {
+ writeEnv(new File("env.txt"));
+ }
+
+ public void writeEnv(File file) throws IOException {
+ try (Sink fileSink = Okio.sink(file);
+ BufferedSink bufferedSink = Okio.buffer(fileSink)) {
+
+ for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
+ bufferedSink.writeUtf8(entry.getKey());
+ bufferedSink.writeUtf8("=");
+ bufferedSink.writeUtf8(entry.getValue());
+ bufferedSink.writeUtf8("\n");
+ }
+
+ }
+ }
+
+ public static void main(String... args) throws Exception {
+ new WriteFile().run();
+ }
+}
diff --git a/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt b/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt
new file mode 100644
index 00000000..86422832
--- /dev/null
+++ b/samples/src/jvmMain/kotlin/okio/samples/BitmapEncoder.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples
+
+import okio.BufferedSink
+import okio.buffer
+import okio.sink
+import java.io.File
+import java.io.IOException
+import kotlin.math.hypot
+
+class KotlinBitmapEncoder {
+ class Bitmap(
+ private val pixels: Array<IntArray>
+ ) {
+ val width: Int = pixels[0].size
+ val height: Int = pixels.size
+
+ fun red(x: Int, y: Int): Int = pixels[y][x] and 0xff0000 shr 16
+
+ fun green(x: Int, y: Int): Int = pixels[y][x] and 0xff00 shr 8
+
+ fun blue(x: Int, y: Int): Int = pixels[y][x] and 0xff
+ }
+
+ /**
+ * Returns a bitmap that lights up red subpixels at the bottom, green subpixels on the right, and
+ * blue subpixels in bottom-right.
+ */
+ fun generateGradient(): Bitmap {
+ val pixels = Array(1080) { IntArray(1920) }
+ for (y in 0 until 1080) {
+ for (x in 0 until 1920) {
+ val r = (y / 1080f * 255).toInt()
+ val g = (x / 1920f * 255).toInt()
+ val b = (hypot(x.toDouble(), y.toDouble()) / hypot(1080.0, 1920.0) * 255).toInt()
+ pixels[y][x] = r shl 16 or (g shl 8) or b
+ }
+ }
+ return Bitmap(pixels)
+ }
+
+ @Throws(IOException::class)
+ fun encode(bitmap: Bitmap, file: File) {
+ file.sink().buffer().use { sink -> encode(bitmap, sink) }
+ }
+
+ /** https://en.wikipedia.org/wiki/BMP_file_format */
+ @Throws(IOException::class)
+ fun encode(bitmap: Bitmap, sink: BufferedSink) {
+ val height = bitmap.height
+ val width = bitmap.width
+ val bytesPerPixel = 3
+ val rowByteCountWithoutPadding = bytesPerPixel * width
+ val rowByteCount = (rowByteCountWithoutPadding + 3) / 4 * 4
+ val pixelDataSize = rowByteCount * height
+ val bmpHeaderSize = 14
+ val dibHeaderSize = 40
+
+ // BMP Header
+ sink.writeUtf8("BM") // ID.
+ sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize) // File size.
+ sink.writeShortLe(0) // Unused.
+ sink.writeShortLe(0) // Unused.
+ sink.writeIntLe(bmpHeaderSize + dibHeaderSize) // Offset of pixel data.
+
+ // DIB Header
+ sink.writeIntLe(dibHeaderSize)
+ sink.writeIntLe(width)
+ sink.writeIntLe(height)
+ sink.writeShortLe(1) // Color plane count.
+ sink.writeShortLe(bytesPerPixel * Byte.SIZE_BITS)
+ sink.writeIntLe(0) // No compression.
+ sink.writeIntLe(16) // Size of bitmap data including padding.
+ sink.writeIntLe(2835) // Horizontal print resolution in pixels/meter. (72 dpi).
+ sink.writeIntLe(2835) // Vertical print resolution in pixels/meter. (72 dpi).
+ sink.writeIntLe(0) // Palette color count.
+ sink.writeIntLe(0) // 0 important colors.
+
+ // Pixel data.
+ for (y in height - 1 downTo 0) {
+ for (x in 0 until width) {
+ sink.writeByte(bitmap.blue(x, y))
+ sink.writeByte(bitmap.green(x, y))
+ sink.writeByte(bitmap.red(x, y))
+ }
+
+ // Padding for 4-byte alignment.
+ for (p in rowByteCountWithoutPadding until rowByteCount) {
+ sink.writeByte(0)
+ }
+ }
+ }
+}
+
+fun main() {
+ val encoder = KotlinBitmapEncoder()
+ val bitmap = encoder.generateGradient()
+ encoder.encode(bitmap, File("gradient.bmp"))
+}
diff --git a/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt b/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt
new file mode 100644
index 00000000..824a01ed
--- /dev/null
+++ b/samples/src/jvmMain/kotlin/okio/samples/ExploreCharsets.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples
+
+import okio.ByteString.Companion.encodeUtf8
+import okio.utf8Size
+import java.io.IOException
+
+@Throws(IOException::class)
+fun dumpStringData(s: String) {
+ println(" " + s)
+ println(" String.length: " + s.length)
+ println("String.codePointCount: " + s.codePointCount(0, s.length))
+ println(" Utf8.size: " + s.utf8Size())
+ println(" UTF-8 bytes: " + s.encodeUtf8().hex())
+ println()
+}
+
+fun main() {
+ dumpStringData("Café \uD83C\uDF69") // NFC: é is one code point.
+ dumpStringData("Café \uD83C\uDF69") // NFD: e is one code point, its accent is another.
+}
diff --git a/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt b/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt
new file mode 100644
index 00000000..2e86ff74
--- /dev/null
+++ b/samples/src/jvmMain/kotlin/okio/samples/GoldenValue.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples
+
+import okio.Buffer
+import okio.ByteString
+import okio.ByteString.Companion.decodeBase64
+import java.io.IOException
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.io.Serializable
+
+class KotlinGoldenValue {
+ fun run() {
+ val point = Point(8.0, 15.0)
+ val pointBytes = serialize(point)
+ println(pointBytes.base64())
+ val goldenBytes = (
+ "rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" +
+ "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA"
+ ).decodeBase64()!!
+ val decoded = deserialize(goldenBytes) as Point
+ assertEquals(point, decoded)
+ }
+
+ @Throws(IOException::class)
+ private fun serialize(o: Any?): ByteString {
+ val buffer = Buffer()
+ ObjectOutputStream(buffer.outputStream()).use { objectOut ->
+ objectOut.writeObject(o)
+ }
+ return buffer.readByteString()
+ }
+
+ @Throws(IOException::class, ClassNotFoundException::class)
+ private fun deserialize(byteString: ByteString): Any? {
+ val buffer = Buffer()
+ buffer.write(byteString)
+ ObjectInputStream(buffer.inputStream()).use { objectIn ->
+ val result = objectIn.readObject()
+ if (objectIn.read() != -1) throw IOException("Unconsumed bytes in stream")
+ return result
+ }
+ }
+
+ internal class Point(var x: Double, var y: Double) : Serializable
+
+ private fun assertEquals(
+ a: Point,
+ b: Point
+ ) {
+ if (a.x != b.x || a.y != b.y) throw AssertionError()
+ }
+}
+
+fun main() {
+ KotlinGoldenValue().run()
+}
diff --git a/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt b/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt
new file mode 100644
index 00000000..3169d4e1
--- /dev/null
+++ b/samples/src/jvmMain/kotlin/okio/samples/Hashing.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples
+
+import okio.Buffer
+import okio.ByteString
+import okio.ByteString.Companion.decodeHex
+import okio.HashingSink.Companion.sha256
+import okio.HashingSource.Companion.sha256
+import okio.blackholeSink
+import okio.buffer
+import okio.source
+import java.io.File
+import java.io.IOException
+
+class KotlinHashing {
+ fun run() {
+ val file = File("../README.md")
+
+ println("ByteString")
+ val byteString = readByteString(file)
+ println(" md5: " + byteString.md5().hex())
+ println(" sha1: " + byteString.sha1().hex())
+ println(" sha256: " + byteString.sha256().hex())
+ println(" sha512: " + byteString.sha512().hex())
+ println()
+
+ println("Buffer")
+ val buffer = readBuffer(file)
+ println(" md5: " + buffer.md5().hex())
+ println(" sha1: " + buffer.sha1().hex())
+ println(" sha256: " + buffer.sha256().hex())
+ println(" sha512: " + buffer.sha512().hex())
+ println()
+
+ println("HashingSource")
+ sha256(file.source()).use { hashingSource ->
+ hashingSource.buffer().use { source ->
+ source.readAll(blackholeSink())
+ println(" sha256: " + hashingSource.hash.hex())
+ }
+ }
+ println()
+
+ println("HashingSink")
+ sha256(blackholeSink()).use { hashingSink ->
+ hashingSink.buffer().use { sink ->
+ file.source().use { source ->
+ sink.writeAll(source)
+ sink.close() // Emit anything buffered.
+ println(" sha256: " + hashingSink.hash.hex())
+ }
+ }
+ }
+ println()
+
+ println("HMAC")
+ val secret = "7065616e7574627574746572".decodeHex()
+ println("hmacSha256: " + byteString.hmacSha256(secret).hex())
+ println()
+ }
+
+ @Throws(IOException::class)
+ fun readByteString(file: File): ByteString {
+ return file.source().buffer().use { it.readByteString() }
+ }
+
+ @Throws(IOException::class)
+ fun readBuffer(file: File): Buffer {
+ return file.source().use { source ->
+ Buffer().also { it.writeAll(source) }
+ }
+ }
+}
+
+fun main() {
+ KotlinHashing().run()
+}
diff --git a/samples/src/jvmMain/kotlin/okio/samples/ReadFileLineByLine.kt b/samples/src/jvmMain/kotlin/okio/samples/ReadFileLineByLine.kt
new file mode 100644
index 00000000..b3fa31ba
--- /dev/null
+++ b/samples/src/jvmMain/kotlin/okio/samples/ReadFileLineByLine.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples
+
+import okio.buffer
+import okio.source
+import java.io.File
+import java.io.IOException
+
+@Throws(IOException::class)
+fun readLines(file: File) {
+ file.source().use { fileSource ->
+ fileSource.buffer().use { bufferedFileSource ->
+ while (true) {
+ val line = bufferedFileSource.readUtf8Line() ?: break
+ if ("square" in line) {
+ println(line)
+ }
+ }
+ }
+ }
+}
+
+fun main() {
+ readLines(File("../README.md"))
+}
diff --git a/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt b/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt
new file mode 100644
index 00000000..d3b786a1
--- /dev/null
+++ b/samples/src/jvmMain/kotlin/okio/samples/SocksProxyServer.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples
+
+import okio.Buffer
+import okio.BufferedSink
+import okio.Sink
+import okio.Source
+import okio.buffer
+import okio.sink
+import okio.source
+import java.io.IOException
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.ProtocolException
+import java.net.Proxy
+import java.net.ServerSocket
+import java.net.Socket
+import java.net.URL
+import java.util.Collections
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executors
+
+private const val VERSION_5 = 5
+private const val METHOD_NO_AUTHENTICATION_REQUIRED = 0
+private const val ADDRESS_TYPE_IPV4 = 1
+private const val ADDRESS_TYPE_DOMAIN_NAME = 3
+private const val COMMAND_CONNECT = 1
+private const val REPLY_SUCCEEDED = 0
+
+/**
+ * A partial implementation of SOCKS Protocol Version 5.
+ * See [RFC 1928](https://www.ietf.org/rfc/rfc1928.txt).
+ */
+class KotlinSocksProxyServer {
+ private val executor = Executors.newCachedThreadPool()
+ private lateinit var serverSocket: ServerSocket
+ private val openSockets: MutableSet<Socket> = Collections.newSetFromMap(ConcurrentHashMap())
+
+ @Throws(IOException::class)
+ fun start() {
+ serverSocket = ServerSocket(0)
+ executor.execute { acceptSockets() }
+ }
+
+ @Throws(IOException::class)
+ fun shutdown() {
+ serverSocket.close()
+ executor.shutdown()
+ }
+
+ fun proxy(): Proxy = Proxy(
+ Proxy.Type.SOCKS,
+ InetSocketAddress.createUnresolved("localhost", serverSocket.localPort)
+ )
+
+ private fun acceptSockets() {
+ try {
+ while (true) {
+ val from = serverSocket.accept()
+ openSockets.add(from)
+ executor.execute { handleSocket(from) }
+ }
+ } catch (e: IOException) {
+ println("shutting down: $e")
+ } finally {
+ for (socket in openSockets) {
+ socket.close()
+ }
+ }
+ }
+
+ private fun handleSocket(fromSocket: Socket) {
+ try {
+ val fromSource = fromSocket.source().buffer()
+ val fromSink = fromSocket.sink().buffer()
+
+ // Read the hello.
+ val socksVersion = fromSource.readByte().toInt() and 0xff
+ if (socksVersion != VERSION_5) throw ProtocolException()
+ val methodCount = fromSource.readByte().toInt() and 0xff
+ var foundSupportedMethod = false
+ for (i in 0 until methodCount) {
+ val method: Int = fromSource.readByte().toInt() and 0xff
+ foundSupportedMethod = foundSupportedMethod or (method == METHOD_NO_AUTHENTICATION_REQUIRED)
+ }
+ if (!foundSupportedMethod) throw ProtocolException()
+
+ // Respond to hello.
+ fromSink.writeByte(VERSION_5)
+ .writeByte(METHOD_NO_AUTHENTICATION_REQUIRED)
+ .emit()
+
+ // Read a command.
+ val version = fromSource.readByte().toInt() and 0xff
+ val command = fromSource.readByte().toInt() and 0xff
+ val reserved = fromSource.readByte().toInt() and 0xff
+ if (version != VERSION_5 || command != COMMAND_CONNECT || reserved != 0) {
+ throw ProtocolException()
+ }
+
+ // Read an address.
+ val addressType = fromSource.readByte().toInt() and 0xff
+ val inetAddress = when (addressType) {
+ ADDRESS_TYPE_IPV4 -> InetAddress.getByAddress(fromSource.readByteArray(4L))
+ ADDRESS_TYPE_DOMAIN_NAME -> {
+ val domainNameLength: Int = fromSource.readByte().toInt() and 0xff
+ InetAddress.getByName(fromSource.readUtf8(domainNameLength.toLong()))
+ }
+ else -> throw ProtocolException()
+ }
+ val port = fromSource.readShort().toInt() and 0xffff
+
+ // Connect to the caller's specified host.
+ val toSocket = Socket(inetAddress, port)
+ openSockets.add(toSocket)
+ val localAddress = toSocket.localAddress.address
+ if (localAddress.size != 4) throw ProtocolException()
+
+ // Write the reply.
+ fromSink.writeByte(VERSION_5)
+ .writeByte(REPLY_SUCCEEDED)
+ .writeByte(0)
+ .writeByte(ADDRESS_TYPE_IPV4)
+ .write(localAddress)
+ .writeShort(toSocket.localPort)
+ .emit()
+
+ // Connect sources to sinks in both directions.
+ val toSink = toSocket.sink()
+ executor.execute { transfer(fromSocket, fromSource, toSink) }
+ val toSource = toSocket.source()
+ executor.execute { transfer(toSocket, toSource, fromSink) }
+ } catch (e: IOException) {
+ fromSocket.close()
+ openSockets.remove(fromSocket)
+ println("connect failed for $fromSocket: $e")
+ }
+ }
+
+ /**
+ * Read data from `source` and write it to `sink`. This doesn't use [BufferedSink.writeAll]
+ * because that method doesn't flush aggressively and we need that.
+ */
+ private fun transfer(sourceSocket: Socket, source: Source, sink: Sink) {
+ try {
+ val buffer = Buffer()
+ var byteCount: Long
+ while (source.read(buffer, 8192L).also { byteCount = it } != -1L) {
+ sink.write(buffer, byteCount)
+ sink.flush()
+ }
+ } catch (e: IOException) {
+ println("transfer failed from $sourceSocket: $e")
+ } finally {
+ sink.close()
+ source.close()
+ sourceSocket.close()
+ openSockets.remove(sourceSocket)
+ }
+ }
+}
+
+fun main() {
+ val proxyServer = KotlinSocksProxyServer()
+ proxyServer.start()
+
+ val url = URL("https://publicobject.com/helloworld.txt")
+ val connection = url.openConnection(proxyServer.proxy())
+ connection.getInputStream().source().buffer().use { source ->
+ generateSequence { source.readUtf8Line() }
+ .forEach(::println)
+ }
+
+ proxyServer.shutdown()
+}
diff --git a/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt b/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt
new file mode 100644
index 00000000..56726932
--- /dev/null
+++ b/samples/src/jvmMain/kotlin/okio/samples/WriteFile.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples
+
+import okio.buffer
+import okio.sink
+import java.io.File
+import java.io.IOException
+
+@Throws(IOException::class)
+fun writeEnv(file: File) {
+ file.sink().buffer().use { sink ->
+ for ((key, value) in System.getenv()) {
+ sink.writeUtf8(key)
+ sink.writeUtf8("=")
+ sink.writeUtf8(value)
+ sink.writeUtf8("\n")
+ }
+ }
+}
+
+fun main() {
+ writeEnv(File("env.txt"))
+}
diff --git a/samples/src/jvmTest/java/okio/samples/ChannelsTest.java b/samples/src/jvmTest/java/okio/samples/ChannelsTest.java
new file mode 100644
index 00000000..8c497475
--- /dev/null
+++ b/samples/src/jvmTest/java/okio/samples/ChannelsTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2018 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.EnumSet;
+import java.util.Set;
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+import okio.Timeout;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static java.nio.file.StandardOpenOption.APPEND;
+import static java.nio.file.StandardOpenOption.READ;
+import static java.nio.file.StandardOpenOption.WRITE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public final class ChannelsTest {
+ private static final String quote =
+ "John, the kind of control you're attempting simply is... it's not "
+ + "possible. If there is one thing the history of evolution has "
+ + "taught us it's that life will not be contained. Life breaks "
+ + "free, it expands to new territories and crashes through "
+ + "barriers, painfully, maybe even dangerously, but, uh... well, "
+ + "there it is.";
+
+ private static final Set<StandardOpenOption> r = EnumSet.of(READ);
+ private static final Set<StandardOpenOption> w = EnumSet.of(WRITE);
+ private static final Set<StandardOpenOption> append = EnumSet.of(WRITE, APPEND);
+
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test public void testReadChannel() throws Exception {
+ ReadableByteChannel channel = new Buffer().writeUtf8(quote);
+
+ Buffer buffer = new Buffer();
+ Source source = new ByteChannelSource(channel, Timeout.NONE);
+ source.read(buffer, 75);
+
+ assertThat(buffer.readUtf8())
+ .isEqualTo("John, the kind of control you're attempting simply is... it's not possible.");
+ }
+
+ @Test public void testReadChannelFully() throws Exception {
+ ReadableByteChannel channel = new Buffer().writeUtf8(quote);
+
+ BufferedSource source = Okio.buffer(new ByteChannelSource(channel, Timeout.NONE));
+ assertThat(source.readUtf8())
+ .isEqualTo(quote);
+ }
+
+ @Test public void testWriteChannel() throws Exception {
+ Buffer channel = new Buffer();
+
+ Sink sink = new ByteChannelSink(channel, Timeout.NONE);
+ sink.write(new Buffer().writeUtf8(quote), 75);
+
+ assertThat(channel.readUtf8())
+ .isEqualTo("John, the kind of control you're attempting simply is... it's not possible.");
+ }
+
+ @Test public void testReadWriteFile() throws Exception {
+ Path path = temporaryFolder.newFile().toPath();
+
+ Sink sink = new FileChannelSink(FileChannel.open(path, w), Timeout.NONE);
+ sink.write(new Buffer().writeUtf8(quote), 317);
+ sink.close();
+ assertTrue(Files.exists(path));
+ assertEquals(quote.length(), Files.size(path));
+
+ Buffer buffer = new Buffer();
+ Source source = new FileChannelSource(FileChannel.open(path, r), Timeout.NONE);
+
+ source.read(buffer, 44);
+ assertThat(buffer.readUtf8())
+ .isEqualTo("John, the kind of control you're attempting ");
+
+ source.read(buffer, 31);
+ assertThat(buffer.readUtf8())
+ .isEqualTo("simply is... it's not possible.");
+ }
+
+ @Test public void testAppend() throws Exception {
+ Path path = temporaryFolder.newFile().toPath();
+
+ Buffer buffer = new Buffer().writeUtf8(quote);
+ Sink sink;
+ BufferedSource source;
+
+ sink = new FileChannelSink(FileChannel.open(path, w), Timeout.NONE);
+ sink.write(buffer, 75);
+ sink.close();
+ assertTrue(Files.exists(path));
+ assertEquals(75, Files.size(path));
+
+ source = Okio.buffer(new FileChannelSource(FileChannel.open(path, r), Timeout.NONE));
+ assertThat(source.readUtf8())
+ .isEqualTo("John, the kind of control you're attempting simply is... it's not possible.");
+
+ sink = new FileChannelSink(FileChannel.open(path, append), Timeout.NONE);
+ sink.write(buffer, buffer.size());
+ sink.close();
+ assertTrue(Files.exists(path));
+ assertEquals(quote.length(), Files.size(path));
+
+ source = Okio.buffer(new FileChannelSource(FileChannel.open(path, r), Timeout.NONE));
+ assertThat(source.readUtf8())
+ .isEqualTo(quote);
+ }
+}
diff --git a/samples/src/jvmTest/java/okio/samples/SourceMarkerTest.java b/samples/src/jvmTest/java/okio/samples/SourceMarkerTest.java
new file mode 100644
index 00000000..cdc363e5
--- /dev/null
+++ b/samples/src/jvmTest/java/okio/samples/SourceMarkerTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio.samples;
+
+import java.io.IOException;
+import java.util.Arrays;
+import okio.Buffer;
+import okio.BufferedSource;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public final class SourceMarkerTest {
+ @Test public void test() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ assertThat(source.readUtf8(3)).isEqualTo("ABC");
+ long pos3 = marker.mark(7); // DEFGHIJ
+ assertThat(source.readUtf8(4)).isEqualTo("DEFG");
+ long pos7 = marker.mark(5); // HIJKL
+ assertThat(source.readUtf8(4)).isEqualTo("HIJK");
+ marker.reset(pos7); // Back to 'H'
+ assertThat(source.readUtf8(3)).isEqualTo("HIJ");
+ marker.reset(pos3); // Back to 'D'
+ assertThat(source.readUtf8(7)).isEqualTo("DEFGHIJ");
+ marker.reset(pos7); // Back to 'H' again.
+ assertThat(source.readUtf8(6)).isEqualTo("HIJKLM");
+ try {
+ marker.reset(pos7);
+ fail();
+ } catch (IOException expected) {
+ assertThat(expected).hasMessage("cannot reset to 7: out of range");
+ }
+ try {
+ marker.reset(pos3);
+ fail();
+ } catch (IOException expected) {
+ assertThat(expected).hasMessage("cannot reset to 3: out of range");
+ }
+ }
+
+ @Test public void exceedLimitTest() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ assertThat(source.readUtf8(3)).isEqualTo("ABC");
+ long pos3 = marker.mark(Long.MAX_VALUE); // D...
+ assertThat(source.readUtf8(4)).isEqualTo("DEFG");
+ long pos7 = marker.mark(5); // H...
+ assertThat(source.readUtf8(4)).isEqualTo("HIJK");
+ marker.reset(pos7); // Back to 'H'
+ assertThat(source.readUtf8(3)).isEqualTo("HIJ");
+ marker.reset(pos3); // Back to 'D'
+ assertThat(source.readUtf8(7)).isEqualTo("DEFGHIJ");
+ marker.reset(pos7); // Back to 'H' again.
+ assertThat(source.readUtf8(6)).isEqualTo("HIJKLM");
+
+ marker.reset(pos7); // Back to 'H' again despite the original limit being exceeded
+ assertThat(source.readUtf8(2)).isEqualTo("HI"); // confirm we're really back at H
+
+ marker.reset(pos3); // Back to 'D' again despite the original limit being exceeded
+ assertThat(source.readUtf8(2)).isEqualTo("DE"); // confirm that we're really back at D
+ }
+
+ @Test public void markAndLimitSmallerThanUserBuffer() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ // Load 5 bytes into the user buffer, then mark 0..3 and confirm that resetting from 4 fails.
+ source.require(5);
+ long pos0 = marker.mark(3);
+ assertThat(source.readUtf8(3)).isEqualTo("ABC");
+ marker.reset(pos0);
+ assertThat(source.readUtf8(4)).isEqualTo("ABCD");
+ try {
+ marker.reset(pos0);
+ fail();
+ } catch (IOException expected) {
+ assertThat(expected).hasMessage("cannot reset to 0: out of range");
+ }
+ }
+
+ @Test public void resetTooLow() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ source.skip(3);
+ marker.mark(3);
+ source.skip(2);
+ try {
+ marker.reset(2);
+ fail();
+ } catch (IOException expected) {
+ assertThat(expected).hasMessage("cannot reset to 2: out of range");
+ }
+ }
+
+ @Test public void resetTooHigh() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ marker.mark(3);
+ source.skip(6);
+ try {
+ marker.reset(4);
+ fail();
+ } catch (IOException expected) {
+ assertThat(expected).hasMessage("cannot reset to 4: out of range");
+ }
+ }
+
+ @Test public void resetUnread() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+
+ marker.mark(3);
+ try {
+ marker.reset(2);
+ fail();
+ } catch (IOException expected) {
+ assertThat(expected).hasMessage("cannot reset to 2: out of range");
+ }
+ }
+
+ @Test public void markNothingBuffered() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ long pos0 = marker.mark(5);
+ assertThat(source.readUtf8(4)).isEqualTo("ABCD");
+ marker.reset(pos0);
+ assertThat(source.readUtf8(6)).isEqualTo("ABCDEF");
+ }
+
+ @Test public void mark0() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ long pos0 = marker.mark(0);
+ marker.reset(pos0);
+ assertThat(source.readUtf8(3)).isEqualTo("ABC");
+ }
+
+ @Test public void markNegative() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+
+ try {
+ marker.mark(-1L);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ assertThat(expected).hasMessage("readLimit < 0: -1");
+ }
+ }
+
+ @Test public void resetAfterEof() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDE"));
+ BufferedSource source = marker.source();
+
+ long pos0 = marker.mark(5);
+ assertThat(source.readUtf8()).isEqualTo("ABCDE");
+ marker.reset(pos0);
+ assertThat(source.readUtf8(3)).isEqualTo("ABC");
+ }
+
+ @Test public void closeThenMark() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ source.close();
+ try {
+ marker.mark(5);
+ fail();
+ } catch (IllegalStateException expected) {
+ assertThat(expected).hasMessage("closed");
+ }
+ }
+
+ @Test public void closeThenReset() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ long pos0 = marker.mark(5);
+ source.close();
+ try {
+ marker.reset(pos0);
+ fail();
+ } catch (IllegalStateException expected) {
+ assertThat(expected).hasMessage("closed");
+ }
+ }
+
+ @Test public void closeThenRead() throws Exception {
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
+ BufferedSource source = marker.source();
+
+ source.close();
+ try {
+ source.readUtf8(3);
+ fail();
+ } catch (IllegalStateException expected) {
+ assertThat(expected).hasMessage("closed");
+ }
+ }
+
+ @Test public void multipleSegments() throws Exception {
+ String as = repeat('a', 10_000);
+ String bs = repeat('b', 10_000);
+ String cs = repeat('c', 10_000);
+ String ds = repeat('d', 10_000);
+
+ SourceMarker marker = new SourceMarker(new Buffer().writeUtf8(as + bs + cs + ds));
+ BufferedSource source = marker.source();
+
+ assertThat(source.readUtf8(10_000)).isEqualTo(as);
+ long pos10k = marker.mark(15_000);
+ assertThat(source.readUtf8(10_000)).isEqualTo(bs);
+ long pos20k = marker.mark(15_000);
+ assertThat(source.readUtf8(10_000)).isEqualTo(cs);
+ marker.reset(pos20k);
+ marker.reset(pos10k);
+ assertThat(source.readUtf8(30_000)).isEqualTo(bs + cs + ds);
+ }
+
+ private String repeat(char c, int count) {
+ char[] array = new char[count];
+ Arrays.fill(array, c);
+ return new String(array);
+ }
+}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..97f9d9e9
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,14 @@
+rootProject.name = 'okio-parent'
+
+include ':okio'
+include ':okio:jvm:japicmp'
+include ':okio:jvm:jmh'
+include ':samples'
+
+enableFeaturePreview("GRADLE_METADATA")
+
+// The Android test module doesn't work in IntelliJ. Use Android Studio or the command line.
+if (properties.containsKey('android.injected.invoked.from.ide') ||
+ System.getenv('ANDROID_SDK_ROOT') != null) {
+ include ':android-test'
+}