diff options
author | arangelov <arangelov@google.com> | 2021-04-19 14:40:09 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2021-04-19 14:40:09 +0000 |
commit | 518e892b1f4228a8d79a6f65138fcb29922ea383 (patch) | |
tree | f23349a7ab3321ab830d3a9021bf74abdcc2fd84 | |
parent | 9591bd1f133e194a299f0eb8b2d14cd809a4e95c (diff) | |
parent | db42b8a834f8a0372dc4f2eaab6dc638a7ca1023 (diff) | |
download | okio-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
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 @@ -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 Binary files differnew file mode 100644 index 00000000..bdc98d1c --- /dev/null +++ b/docs/images/icon-square.png diff --git a/docs/images/logo-square.png b/docs/images/logo-square.png Binary files differnew file mode 100644 index 00000000..788b301a --- /dev/null +++ b/docs/images/logo-square.png 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 Binary files differnew file mode 100644 index 00000000..e708b1c0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar 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 © 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' +} |