Add okio library

Bug: 184695627
Test: compiled
Change-Id: Ic52dd5fa28b9da8fa9b0f7983e9fda8ed4030f36
diff --git a/.buildscript/prepare_mkdocs.sh b/.buildscript/prepare_mkdocs.sh
new file mode 100755
index 0000000..5dcb42c
--- /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 0000000..220fed8
--- /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 0000000..15d8bd6
--- /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 0000000..26b2689
--- /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 0000000..451ec21
--- /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 0000000..cf49f1c
--- /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 0000000..c8c3b94
--- /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 0000000..48028d0
--- /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 0000000..ac16ed7
--- /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 0000000..85de3d4
--- /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 0000000..d645695
--- /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 0000000..659dfff
--- /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 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..25abe55
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,3 @@
+arangelov@google.com
+stanleytfwang@google.com
+alexkershaw@google.com
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6a331ee
--- /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 0000000..f624005
--- /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 0000000..56fcda7
--- /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 0000000..ace307d
--- /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 0000000..fe95031
--- /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 0000000..3f2b0bb
--- /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 0000000..353fa80
--- /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 0000000..4923686
--- /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 0000000..6a97690
--- /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 0000000..48136b7
--- /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 0000000..ae3a99e
--- /dev/null
+++ b/docs/css/dokka-logo.css
@@ -0,0 +1,6 @@
+#logo {
+    background-image: url(../images/logo-square.png);
+    background-size: auto;
+    padding-top: unset;
+    height: 60px;
+}
diff --git a/docs/images/icon-square.png b/docs/images/icon-square.png
new file mode 100644
index 0000000..bdc98d1
--- /dev/null
+++ b/docs/images/icon-square.png
Binary files differ
diff --git a/docs/images/logo-square.png b/docs/images/logo-square.png
new file mode 100644
index 0000000..788b301
--- /dev/null
+++ b/docs/images/logo-square.png
Binary files differ
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..d69c409
--- /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é šŸ©                     | CafeĢ šŸ©                        |
+| --------------------: | :---------------------------| :------------------------------|
+|                  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 0000000..cad6863
--- /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 0000000..94a1a34
--- /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 0000000..1eb8149
--- /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 0000000..21f92f0
--- /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 0000000..7ec9429
--- /dev/null
+++ b/gradle/gradle-mvn-mpp-push.gradle
@@ -0,0 +1,137 @@
+apply plugin: 'maven-publish'
+apply plugin: 'signing'
+apply plugin: 'org.jetbrains.dokka'
+
+def dokkaConfiguration = {
+  outputDirectory.set(file("$rootDir/docs/2.x"))
+
+  dokkaSourceSets {
+    configureEach {
+      reportUndocumented.set(false)
+      skipDeprecated.set(true)
+      jdkVersion.set(8)
+      perPackageOption {
+        matchingRegex.set("com\\.squareup.okio.*")
+        suppress.set(true)
+      }
+      perPackageOption {
+        matchingRegex.set("okio\\.internal.*")
+        suppress.set(true)
+      }
+    }
+  }
+}
+
+dokkaGfm.configure(dokkaConfiguration)
+dokkaHtml.configure(dokkaConfiguration)
+
+def rootRelativePath(path) {
+  return rootProject.file(path).toString().replace('\\', '/')
+}
+
+dokkaHtml.pluginsMapConfiguration.set([
+  "org.jetbrains.dokka.base.DokkaBase": """{ "customStyleSheets": ["${rootRelativePath("docs/css/dokka-logo.css")}"], "customAssets" : ["${rootRelativePath("docs/images/logo-square.png")}"]}"""
+])
+
+def isReleaseBuild() {
+  return VERSION_NAME.contains("SNAPSHOT") == false
+}
+
+def getReleaseRepositoryUrl() {
+  return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL :
+          "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
+}
+
+def getSnapshotRepositoryUrl() {
+  return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL :
+          "https://oss.sonatype.org/content/repositories/snapshots/"
+}
+
+def getRepositoryUsername() {
+  return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : ""
+}
+
+def getRepositoryPassword() {
+  return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : ""
+}
+
+task emptySourcesJar(type: Jar) {
+  classifier = 'sources'
+}
+
+task javadocsJar(type: Jar, dependsOn: dokkaGfm) {
+  classifier = 'javadoc'
+  from dokkaGfm.outputDirectory
+}
+
+signing {
+  required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
+  sign(publishing.publications)
+}
+
+publishing {
+  publications.all {
+    artifact javadocsJar
+
+    pom.withXml {
+      def root = asNode()
+
+      root.children().last() + {
+        resolveStrategy = Closure.DELEGATE_FIRST
+
+        description POM_DESCRIPTION
+        name POM_NAME
+        url POM_URL
+        licenses {
+          license {
+            name POM_LICENCE_NAME
+            url POM_LICENCE_URL
+            distribution POM_LICENCE_DIST
+          }
+        }
+        scm {
+          url POM_SCM_URL
+          connection POM_SCM_CONNECTION
+          developerConnection POM_SCM_DEV_CONNECTION
+        }
+        developers {
+          developer {
+            id POM_DEVELOPER_ID
+            name POM_DEVELOPER_NAME
+          }
+        }
+      }
+    }
+  }
+  
+  // Use default artifact name for the JVM target
+  publications {
+    kotlinMultiplatform {
+      artifactId = POM_ARTIFACT_ID + '-multiplatform'
+    }
+    jvm {
+      artifactId = POM_ARTIFACT_ID
+    }
+  }
+
+  afterEvaluate {
+    publications.getByName('kotlinMultiplatform') {
+      // Source jars are only created for platforms, not the common artifact.
+      artifact emptySourcesJar
+    }
+  }
+
+  repositories {
+    maven {
+      url isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl()
+      credentials {
+        username getRepositoryUsername()
+        password getRepositoryPassword()
+      }
+    }
+    maven {
+      name 'test'
+      url "file://${rootProject.buildDir}/localMaven"
+    }
+  }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..4d9ca16
--- /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 0000000..4f906e0
--- /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 0000000..ac1b06f
--- /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 0000000..11fd77f
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,59 @@
+site_name: Okio
+repo_name: Okio
+repo_url: https://github.com/square/okio
+site_description: "A modern I/O library for Android, Kotlin, and Java."
+site_author: Square, Inc.
+remote_branch: gh-pages
+edit_uri: ""
+
+copyright: 'Copyright &copy; 2019 Square, Inc.'
+
+theme:
+  name: 'material'
+  favicon: images/icon-square.png
+  logo: images/icon-square.png
+  palette:
+    primary: 'deep purple'
+    accent: 'white'
+  icon:
+    repo: fontawesome/brands/github
+
+extra:
+  social:
+    - icon: fontawesome/brands/twitter
+      link: https://twitter.com/squareeng
+    - icon: fontawesome/brands/stack-overflow
+      link: https://stackoverflow.com/questions/tagged/okio?sort=active
+
+extra_css:
+  - 'css/app.css'
+
+markdown_extensions:
+  - smarty
+  - codehilite:
+      guess_lang: false
+  - footnotes
+  - meta
+  - toc:
+      permalink: true
+  - pymdownx.betterem:
+      smart_enable: all
+  - pymdownx.caret
+  - pymdownx.inlinehilite
+  - pymdownx.magiclink
+  - pymdownx.smartsymbols
+  - pymdownx.superfences
+  - pymdownx.tabbed
+  - pymdownx.tilde
+  - tables
+
+nav:
+  - 'Overview': index.md
+  - 'Stack Overflow ā': https://stackoverflow.com/questions/tagged/okio?sort=active
+  - '2.x API': 2.x/okio/okio/index.html
+  - '1.x API ā': https://square.github.io/okio/1.x/okio/
+  - 'Change Log': changelog.md
+  - 'Multiplatform': multiplatform.md
+  - 'Contributing': contributing.md
+  - 'Code of Conduct': code_of_conduct.md
+
diff --git a/okio/build.gradle b/okio/build.gradle
new file mode 100644
index 0000000..980ecb0
--- /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 0000000..775966e
--- /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 0000000..782c699
--- /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 0000000..d7c6d77
--- /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 0000000..54eebdd
--- /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 0000000..5c7b811
--- /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 0000000..8f6c007
--- /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 0000000..22a73a2
--- /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 0000000..61ea059
--- /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",
+        "Սļ½, I'll š“½š–¾ll į¶ŒÖ…š˜‚ į“›ā„Žā„® šœšš•£ą„¦ļ½‚lš–¾ļ½ ļ½—Ń–š•„š’½ š˜µš˜©šž š“¼š™˜š¢š”¢š“·š—šœ„ššš‘–ļ½ƒ š› š¾ļ½—ššŽš‘Ÿ š•„ļ½ˆāŗšžƒ š›„š“øš˜‚'š’“š—² υš–˜š“²š—‡É” š•™ššŽš‘Ÿļ½…, "
+            + "š›Šš“½ ā…†š•šššæ'š— š”Æš™šš™¦į“œšœ¾š’“š˜¦ š”žš˜Æš² Ōšœ„š‘ ššŒιš˜±lιš’ļ½… š‘”šœŽ š•’ššš–™š“Ŗіšž¹ š”¦šš. š’€οš—Ž š”Æš‘’āŗš–‰ ļ½—š”š°š”± šž‚šž½Ņ»š“®š“‡Ę½ Õ°š–ŗš–‰ ā…¾š›š…ā…‡ š°πŌ š”‚į“‘į“œ š“‰ļ®Øį€šš” "
+            + "тš’½š‘’ š—‡š•–ā…¹šš š”°š’•Šµš“…. š˜ ā²Ÿš–š š–‰ā…°Ōš•'τ š™šššŠļ½’šž¹ š˜µį‚š–¾ š’š§Ł‡ļ½—lš‘’š–‰Ęš™š š“Æą«¦ļ½’ š”‚šž¼š’–š•£š‘ š•–lš™«š–Šš“¼, š‘ˆŠ¾ ļ½™š˜°š’– ā…†Ū•š—‡'ļ½” šœαš’Œš•– š›‚šŸ‰ā„½ "
+            + "š«ā…‡š—Œā²£ą¹ϖš–˜ź™‡į–Æš“²lš“²š’•š˜† šŸšž¼š˜³ šš¤š‘”. š›¶š›”š”² ļ½“š•„σσš ļ®©š•Ÿ š’•š—š”¢ š˜“š”šœŽį“œlā…¾š“®š”Æššœ š›š™› į¶ƒššŽį“ØįŽ„Õ½ššœš˜¦š“ˆ š“½šžø ļ½š’„ššŒšžøļ½ρlš›Šźœ±š” š“ˆšš˜ļ½ššŽšžƒš”„ā³šž¹š”¤ ššš—Œ š–‹ļ½š¬š’• "
+            + "αļ½“ γš›š•¦ š” ļ»«š›–lŌ, ššŠπš‘‘ Š¬š‘’š™›ą«¦š“‡š˜¦ š“ŽŁ„š–š ā…‡ļ½–ā„Æš… šœ…Õøš’†ļ½— ļ½—š—µš’‚š˜ į¶Œą©¦š—Ž ļ½ˆššš—±, šœøļ®Øš’– š“¹š°š”±š–¾š—‡š“½š”¢ā…† іš•„, ššŠšœ›š“­ š“¹š–ŗā…½Ļ°š˜¢ā„ŠŠµį§ š‘–šžƒ, "
+            + "ššš›‘ź“’ š™Ølš”žŃ€š˜±š”¢š“­ É©š— Ūš›‘ š•’ ļ½lš›‚Ń•į“›š—‚šœ lšž„ā„¼š” š’½š‘ļ®ŖāØÆ, š”žϖš’¹ ļ½Žš›”ļ½— š›¾šØšž„'š—暝”¢ źœ±ā„®llš™žļ½ŽÉ” É©š˜, š™®š• š›– ļ½—š‘Žā„¼šš—š›‚ š•¤š“®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 0000000..2a51635
--- /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 0000000..dc11738
--- /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 0000000..c628b19
--- /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 0000000..466f7aa
--- /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 0000000..394c214
--- /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 0000000..eeb7267
--- /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 0000000..db0b1ef
--- /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",
+        "Սļ½, I'll š“½š–¾ll į¶ŒÖ…š˜‚ į“›ā„Žā„® šœšš•£ą„¦ļ½‚lš–¾ļ½ ļ½—Ń–š•„š’½ š˜µš˜©šž š“¼š™˜š¢š”¢š“·š—šœ„ššš‘–ļ½ƒ š› š¾ļ½—ššŽš‘Ÿ š•„ļ½ˆāŗšžƒ š›„š“øš˜‚'š’“š—² υš–˜š“²š—‡É” š•™ššŽš‘Ÿļ½…, "
+            + "š›Šš“½ ā…†š•šššæ'š— š”Æš™šš™¦į“œšœ¾š’“š˜¦ š”žš˜Æš² Ōšœ„š‘ ššŒιš˜±lιš’ļ½… š‘”šœŽ š•’ššš–™š“Ŗіšž¹ š”¦šš. š’€οš—Ž š”Æš‘’āŗš–‰ ļ½—š”š°š”± šž‚šž½Ņ»š“®š“‡Ę½ Õ°š–ŗš–‰ ā…¾š›š…ā…‡ š°πŌ š”‚į“‘į“œ š“‰ļ®Øį€šš” "
+            + "тš’½š‘’ š—‡š•–ā…¹šš š”°š’•Šµš“…. š˜ ā²Ÿš–š š–‰ā…°Ōš•'τ š™šššŠļ½’šž¹ š˜µį‚š–¾ š’š§Ł‡ļ½—lš‘’š–‰Ęš™š š“Æą«¦ļ½’ š”‚šž¼š’–š•£š‘ š•–lš™«š–Šš“¼, š‘ˆŠ¾ ļ½™š˜°š’– ā…†Ū•š—‡'ļ½” šœαš’Œš•– š›‚šŸ‰ā„½ "
+            + "š«ā…‡š—Œā²£ą¹ϖš–˜ź™‡į–Æš“²lš“²š’•š˜† šŸšž¼š˜³ šš¤š‘”. š›¶š›”š”² ļ½“š•„σσš ļ®©š•Ÿ š’•š—š”¢ š˜“š”šœŽį“œlā…¾š“®š”Æššœ š›š™› į¶ƒššŽį“ØįŽ„Õ½ššœš˜¦š“ˆ š“½šžø ļ½š’„ššŒšžøļ½ρlš›Šźœ±š” š“ˆšš˜ļ½ššŽšžƒš”„ā³šž¹š”¤ ššš—Œ š–‹ļ½š¬š’• "
+            + "αļ½“ γš›š•¦ š” ļ»«š›–lŌ, ššŠπš‘‘ Š¬š‘’š™›ą«¦š“‡š˜¦ š“ŽŁ„š–š ā…‡ļ½–ā„Æš… šœ…Õøš’†ļ½— ļ½—š—µš’‚š˜ į¶Œą©¦š—Ž ļ½ˆššš—±, šœøļ®Øš’– š“¹š°š”±š–¾š—‡š“½š”¢ā…† іš•„, ššŠšœ›š“­ š“¹š–ŗā…½Ļ°š˜¢ā„ŠŠµį§ š‘–šžƒ, "
+            + "ššš›‘ź“’ š™Ølš”žŃ€š˜±š”¢š“­ É©š— Ūš›‘ š•’ ļ½lš›‚Ń•į“›š—‚šœ lšž„ā„¼š” š’½š‘ļ®ŖāØÆ, š”žϖš’¹ ļ½Žš›”ļ½— š›¾šØšž„'š—暝”¢ źœ±ā„®llš™žļ½ŽÉ” É©š˜, š™®š• š›– ļ½—š‘Žā„¼šš—š›‚ š•¤š“®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 0000000..4cc1fb6
--- /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 0000000..cd81891
--- /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 0000000..eb14103
--- /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 0000000..5ff03b2
--- /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 0000000..98db0b5
--- /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 0000000..4790d3c
--- /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 0000000..9ab3688
--- /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 0000000..16394d2
--- /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 0000000..40c2658
--- /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 0000000..0ba4d15
--- /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 0000000..7eb34c6
--- /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 0000000..7fc3a4a
--- /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 0000000..06cb6bf
--- /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 0000000..9c61c99
--- /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 0000000..116678a
--- /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 0000000..9ce0739
--- /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 0000000..598d83d
--- /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 0000000..80e3fae
--- /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 0000000..b626e42
--- /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 0000000..3687949
--- /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 0000000..f21c7a2
--- /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 0000000..bda4f4a
--- /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 0000000..0a6af54
--- /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 0000000..97c6681
--- /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 0000000..62600bb
--- /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 0000000..ca20e21
--- /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 0000000..926f185
--- /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 0000000..0cb15cc
--- /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 0000000..7a1a488
--- /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 0000000..49b0c4d
--- /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 0000000..4b90143
--- /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 0000000..f46e138
--- /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 0000000..49bff8d
--- /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 0000000..b15d369
--- /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 0000000..842faff
--- /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 0000000..8f4f29a
--- /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 0000000..b983620
--- /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 0000000..bbf6cc6
--- /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 0000000..c75c458
--- /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 = (
+      "Սļ½, I'll š“½š–¾ll į¶ŒÖ…š˜‚ į“›ā„Žā„® šœšš•£ą„¦ļ½‚lš–¾ļ½ ļ½—Ń–š•„š’½ š˜µš˜©šž š“¼š™˜š¢š”¢š“·š—šœ„ššš‘–ļ½ƒ š› š¾ļ½—ššŽš‘Ÿ š•„ļ½ˆāŗšžƒ š›„š“øš˜‚'š’“š—² υš–˜š“²š—‡É” š•™ššŽš‘Ÿļ½…, " +
+        "š›Šš“½ ā…†š•šššæ'š— š”Æš™šš™¦į“œšœ¾š’“š˜¦ š”žš˜Æš² Ōšœ„š‘ ššŒιš˜±lιš’ļ½… š‘”šœŽ š•’ššš–™š“Ŗіšž¹ š”¦šš. š’€οš—Ž š”Æš‘’āŗš–‰ ļ½—š”š°š”± šž‚šž½Ņ»š“®š“‡Ę½ Õ°š–ŗš–‰ ā…¾š›š…ā…‡ š°πŌ š”‚į“‘į“œ š“‰ļ®Øį€šš” " +
+        "тš’½š‘’ š—‡š•–ā…¹šš š”°š’•Šµš“…. š˜ ā²Ÿš–š š–‰ā…°Ōš•'τ š™šššŠļ½’šž¹ š˜µį‚š–¾ š’š§Ł‡ļ½—lš‘’š–‰Ęš™š š“Æą«¦ļ½’ š”‚šž¼š’–š•£š‘ š•–lš™«š–Šš“¼, š‘ˆŠ¾ ļ½™š˜°š’– ā…†Ū•š—‡'ļ½” šœαš’Œš•– š›‚šŸ‰ā„½ " +
+        "š«ā…‡š—Œā²£ą¹ϖš–˜ź™‡į–Æš“²lš“²š’•š˜† šŸšž¼š˜³ šš¤š‘”. š›¶š›”š”² ļ½“š•„σσš ļ®©š•Ÿ š’•š—š”¢ š˜“š”šœŽį“œlā…¾š“®š”Æššœ š›š™› į¶ƒššŽį“ØįŽ„Õ½ššœš˜¦š“ˆ š“½šžø ļ½š’„ššŒšžøļ½ρlš›Šźœ±š” š“ˆšš˜ļ½ššŽšžƒš”„ā³šž¹š”¤ ššš—Œ š–‹ļ½š¬š’• " +
+        "αļ½“ γš›š•¦ š” ļ»«š›–lŌ, ššŠπš‘‘ Š¬š‘’š™›ą«¦š“‡š˜¦ š“ŽŁ„š–š ā…‡ļ½–ā„Æš… šœ…Õøš’†ļ½— ļ½—š—µš’‚š˜ į¶Œą©¦š—Ž ļ½ˆššš—±, šœøļ®Øš’– š“¹š°š”±š–¾š—‡š“½š”¢ā…† іš•„, ššŠšœ›š“­ š“¹š–ŗā…½Ļ°š˜¢ā„ŠŠµį§ š‘–šžƒ, " +
+        "ššš›‘ź“’ š™Ølš”žŃ€š˜±š”¢š“­ É©š— Ūš›‘ š•’ ļ½lš›‚Ń•į“›š—‚šœ lšž„ā„¼š” š’½š‘ļ®ŖāØÆ, š”žϖš’¹ ļ½Žš›”ļ½— š›¾šØšž„'š—暝”¢ źœ±ā„®llš™žļ½ŽÉ” É©š˜, š™®š• š›– ļ½—š‘Žā„¼šš—š›‚ š•¤š“®ll š™žš“‰."
+      )
+    assertEquals(
+      "[size=1496 text=Սļ½, I'll š“½š–¾ll į¶ŒÖ…š˜‚ į“›ā„Žā„® šœšš•£ą„¦ļ½‚lš–¾ļ½ ļ½—Ń–š•„š’½ š˜µš˜©šž š“¼š™˜š¢š”¢š“·š—šœ„ššš‘–ļ½ƒ š› š¾ļ½—ššŽš‘Ÿ š•„ļ½ˆāŗšžƒ " +
+        "š›„š“øš˜‚…]",
+      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 0000000..292e3c5
--- /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 0000000..131dffb
--- /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 0000000..bb45321
--- /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 0000000..abbbae8
--- /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 0000000..4663919
--- /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 0000000..31cf550
--- /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 0000000..db1aeee
--- /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 0000000..83e2e26
--- /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 0000000..1cae58d
--- /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 0000000..7e099f4
--- /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 0000000..680ebdc
--- /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 0000000..1ade417
--- /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 0000000..953eef8
--- /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 0000000..5948ab0
--- /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 0000000..95e3c5d
--- /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 0000000..e43e447
--- /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 0000000..e9a8de1
--- /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 0000000..aa0d24d
--- /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 0000000..390a50a
--- /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 0000000..7b6835e
--- /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 0000000..5f95547
--- /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 0000000..b4bc757
--- /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 0000000..4edb3ce
--- /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 0000000..2077832
--- /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 0000000..857c983
--- /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 0000000..4aa1bb0
--- /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 0000000..b312d56
--- /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 0000000..0edad9c
--- /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 0000000..aa48284
--- /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 0000000..154371f
--- /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 0000000..e71cdff
--- /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 0000000..8f0eb2f
--- /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 0000000..30a47f6
--- /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 0000000..23d83aa
--- /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 0000000..db87daf
--- /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 0000000..ff1e3d3
--- /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 0000000..36bfd2b
--- /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 0000000..25b695d
--- /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 0000000..6fe1feb
--- /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 0000000..25d5516
--- /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 0000000..43c23bf
--- /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 0000000..7df3f93
--- /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 0000000..109ef14
--- /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 0000000..7a7b049
--- /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 0000000..bce9d5a
--- /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 0000000..e93ffb5
--- /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 0000000..dbb83fe
--- /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 0000000..c522380
--- /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 0000000..2b69834
--- /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 0000000..974218b
--- /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 0000000..4cad858
--- /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 0000000..f5586d4
--- /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 0000000..357b992
--- /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 0000000..e013284
--- /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 0000000..470a2dd
--- /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 0000000..149ae64
--- /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 0000000..f1b6624
--- /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 = "Սļ½, I'll š“½š–¾ll į¶ŒÖ…š˜‚ į“›ā„Žā„® šœšš•£ą„¦ļ½‚lš–¾ļ½ ļ½—Ń–š•„š’½ š˜µš˜©šž š“¼š™˜š¢š”¢š“·š—šœ„ššš‘–ļ½ƒ š› š¾ļ½—ššŽš‘Ÿ š•„ļ½ˆāŗšžƒ š›„š“øš˜‚'š’“š—² υš–˜š“²š—‡É” š•™ššŽš‘Ÿļ½…, "
+        + "š›Šš“½ ā…†š•šššæ'š— š”Æš™šš™¦į“œšœ¾š’“š˜¦ š”žš˜Æš² Ōšœ„š‘ ššŒιš˜±lιš’ļ½… š‘”šœŽ š•’ššš–™š“Ŗіšž¹ š”¦šš. š’€οš—Ž š”Æš‘’āŗš–‰ ļ½—š”š°š”± šž‚šž½Ņ»š“®š“‡Ę½ Õ°š–ŗš–‰ ā…¾š›š…ā…‡ š°πŌ š”‚į“‘į“œ š“‰ļ®Øį€šš” "
+        + "тš’½š‘’ š—‡š•–ā…¹šš š”°š’•Šµš“…. š˜ ā²Ÿš–š š–‰ā…°Ōš•'τ š™šššŠļ½’šž¹ š˜µį‚š–¾ š’š§Ł‡ļ½—lš‘’š–‰Ęš™š š“Æą«¦ļ½’ š”‚šž¼š’–š•£š‘ š•–lš™«š–Šš“¼, š‘ˆŠ¾ ļ½™š˜°š’– ā…†Ū•š—‡'ļ½” šœαš’Œš•– š›‚šŸ‰ā„½ "
+        + "š«ā…‡š—Œā²£ą¹ϖš–˜ź™‡į–Æš“²lš“²š’•š˜† šŸšž¼š˜³ šš¤š‘”. š›¶š›”š”² ļ½“š•„σσš ļ®©š•Ÿ š’•š—š”¢ š˜“š”šœŽį“œlā…¾š“®š”Æššœ š›š™› į¶ƒššŽį“ØįŽ„Õ½ššœš˜¦š“ˆ š“½šžø ļ½š’„ššŒšžøļ½ρlš›Šźœ±š” š“ˆšš˜ļ½ššŽšžƒš”„ā³šž¹š”¤ ššš—Œ š–‹ļ½š¬š’• "
+        + "αļ½“ γš›š•¦ š” ļ»«š›–lŌ, ššŠπš‘‘ Š¬š‘’š™›ą«¦š“‡š˜¦ š“ŽŁ„š–š ā…‡ļ½–ā„Æš… šœ…Õøš’†ļ½— ļ½—š—µš’‚š˜ į¶Œą©¦š—Ž ļ½ˆššš—±, šœøļ®Øš’– š“¹š°š”±š–¾š—‡š“½š”¢ā…† іš•„, ššŠšœ›š“­ š“¹š–ŗā…½Ļ°š˜¢ā„ŠŠµį§ š‘–šžƒ, "
+        + "ššš›‘ź“’ š™Ølš”žŃ€š˜±š”¢š“­ É©š— Ūš›‘ š•’ ļ½lš›‚Ń•į“›š—‚šœ lšž„ā„¼š” š’½š‘ļ®ŖāØÆ, š”žϖš’¹ ļ½Žš›”ļ½— š›¾šØšž„'š—暝”¢ źœ±ā„®llš™žļ½ŽÉ” É©š˜, š™®š• š›– ļ½—š‘Žā„¼šš—š›‚ š•¤š“®ll š™žš“‰.";
+    assertEquals( "[size=1496 text=Սļ½, I'll š“½š–¾ll į¶ŒÖ…š˜‚ į“›ā„Žā„® šœšš•£ą„¦ļ½‚lš–¾ļ½ ļ½—Ń–š•„š’½ š˜µš˜©šž š“¼š™˜š¢š”¢š“·š—šœ„ššš‘–ļ½ƒ š› š¾ļ½—ššŽš‘Ÿ š•„ļ½ˆāŗšžƒ "
+        + "š›„š“øš˜‚…]", 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 0000000..f0a31f0
--- /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 0000000..45536fc
--- /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 0000000..848ff02
--- /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 0000000..69b81e3
--- /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 0000000..0486638
--- /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 0000000..b9be1a2
--- /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 0000000..962d011
--- /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 0000000..aec1773
--- /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 0000000..71a4447
--- /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 0000000..030e6ba
--- /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 0000000..9cc177f
--- /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 0000000..6a6aadc
--- /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 0000000..63e0b7d
--- /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 0000000..e440528
--- /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 0000000..0c5a7f4
--- /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 0000000..ddbc4c5
--- /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 0000000..e1533d2
--- /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 0000000..eda7989
--- /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 0000000..c554251
--- /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 0000000..f9f42b0
--- /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 0000000..7b93c65
--- /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 0000000..f273971
--- /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 0000000..f97258e
--- /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 0000000..7a1744a
--- /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 0000000..4db02c5
--- /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 0000000..6bbe9b5
--- /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 0000000..58c6bca
--- /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 0000000..3a41e74
--- /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 0000000..73e74d9
--- /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 0000000..4f4a022
--- /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 0000000..f9f61e6
--- /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 0000000..aab94b3
--- /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 0000000..0b15179
--- /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 0000000..90fcd36
--- /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 0000000..ec28f63
--- /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 0000000..65d717c
--- /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 0000000..98b7718
--- /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 0000000..f0f038b
--- /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 0000000..cbd14a2
--- /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 0000000..62bfd60
--- /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 0000000..ed03094
--- /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 0000000..d6f4b94
--- /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 0000000..50e7099
--- /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 0000000..fe71890
--- /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 0000000..d114647
--- /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 0000000..d66a027
--- /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 0000000..efaef19
--- /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 0000000..a505f6d
--- /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 0000000..ba4a9af
--- /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 0000000..f1f2875
--- /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 0000000..3b20295
--- /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("CafeĢ \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 0000000..b810a32
--- /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 0000000..db5ec93
--- /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 0000000..aeeaa0f
--- /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 0000000..0f2b447
--- /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 0000000..85cbbba
--- /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 0000000..631ebc9
--- /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 0000000..b195a51
--- /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 0000000..d770d82
--- /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 0000000..8deef05
--- /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 0000000..e613abe
--- /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 0000000..8642283
--- /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 0000000..824a01e
--- /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("CafeĢ \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 0000000..2e86ff7
--- /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 0000000..3169d4e
--- /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 0000000..b3fa31b
--- /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 0000000..d3b786a
--- /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 0000000..5672693
--- /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 0000000..8c49747
--- /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 0000000..cdc363e
--- /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 0000000..97f9d9e
--- /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'
+}