Repository: Kotlin/kotlinx-io
Pull request #148 (2023-06-23 13:20:46)

ByteString implementation
Implemented ByteString - an immutable wrapper around byte sequence and added base support to the core IO library.

Missing stuff:
- [x] Module documentation


Closes https://github.com/Kotlin/kotlinx-io/issues/133


Patch:
diff --git a/README.md b/README.md
index f5404be5..31d3e03d 100644
--- a/README.md
+++ b/README.md
@@ -56,4 +56,12 @@ Read the [Contributing Guidelines](CONTRIBUTING.md).
 This project and the corresponding community are governed by the [JetBrains Open Source and Community Code of Conduct](https://confluence.jetbrains.com/display/ALL/JetBrains+Open+Source+and+Community+Code+of+Conduct). Please make sure you read it.
 
 ## License
-kotlinx-io is licensed under the [Apache 2.0 License](LICENSE).
\ No newline at end of file
+kotlinx-io is licensed under the [Apache 2.0 License](LICENSE).
+
+## Credits
+
+Thanks to everyone involved in the project.
+
+A honorable mention goes to the developers of [Okio](https://square.github.io/okio/) 
+that served as the foundation for `kotlinx-io` and to [Jesse Wilson](https://github.com/swankjesse),
+for the help with `Okio` adaption, his suggestions, assistance and guidance with `kotlinx-io` development.
diff --git a/benchmarks/src/commonMain/kotlin/BufferOps.kt b/benchmarks/src/commonMain/kotlin/BufferOps.kt
index fc1d6d58..cffcc673 100644
--- a/benchmarks/src/commonMain/kotlin/BufferOps.kt
+++ b/benchmarks/src/commonMain/kotlin/BufferOps.kt
@@ -214,8 +214,8 @@ open class Utf8StringBenchmark : BufferRWBenchmarkBase() {
     @Benchmark
     fun benchmark(): String {
         val s = buffer.size
-        buffer.writeUtf8(string)
-        return buffer.readUtf8(buffer.size - s)
+        buffer.writeString(string)
+        return buffer.readString(buffer.size - s)
     }
 }
 
@@ -257,16 +257,16 @@ open class Utf8LineBenchmarkBase : BufferRWBenchmarkBase() {
 open class Utf8LineBenchmark : Utf8LineBenchmarkBase() {
     @Benchmark
     fun benchmark(): String? {
-        buffer.writeUtf8(string)
-        return buffer.readUtf8Line()
+        buffer.writeString(string)
+        return buffer.readLine()
     }
 }
 
 open class Utf8LineStrictBenchmark : Utf8LineBenchmarkBase() {
     @Benchmark
     fun benchmark(): String {
-        buffer.writeUtf8(string)
-        return buffer.readUtf8LineStrict()
+        buffer.writeString(string)
+        return buffer.readLineStrict()
     }
 }
 
diff --git a/core/api/kotlinx-io-core.api b/core/api/kotlinx-io-core.api
index 75e8d908..38cb45ff 100644
--- a/core/api/kotlinx-io-core.api
+++ b/core/api/kotlinx-io-core.api
@@ -47,6 +47,19 @@ public final class kotlinx/io/BufferExtJvmKt {
 	public static final fun write (Lkotlinx/io/Buffer;Ljava/io/InputStream;J)Lkotlinx/io/Buffer;
 }
 
+public final class kotlinx/io/BufferExtKt {
+	public static final fun snapshot (Lkotlinx/io/Buffer;)Lkotlinx/io/bytestring/ByteString;
+}
+
+public final class kotlinx/io/ByteStringExtKt {
+	public static final fun indexOf (Lkotlinx/io/Source;Lkotlinx/io/bytestring/ByteString;J)J
+	public static synthetic fun indexOf$default (Lkotlinx/io/Source;Lkotlinx/io/bytestring/ByteString;JILjava/lang/Object;)J
+	public static final fun readByteString (Lkotlinx/io/Source;)Lkotlinx/io/bytestring/ByteString;
+	public static final fun readByteString (Lkotlinx/io/Source;I)Lkotlinx/io/bytestring/ByteString;
+	public static final fun write (Lkotlinx/io/Sink;Lkotlinx/io/bytestring/ByteString;II)V
+	public static synthetic fun write$default (Lkotlinx/io/Sink;Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)V
+}
+
 public final class kotlinx/io/CoreKt {
 	public static final fun buffered (Lkotlinx/io/RawSink;)Lkotlinx/io/Sink;
 	public static final fun buffered (Lkotlinx/io/RawSource;)Lkotlinx/io/Source;
@@ -162,14 +175,14 @@ public final class kotlinx/io/SourceExtKt {
 }
 
 public final class kotlinx/io/Utf8Kt {
-	public static final fun readUtf8 (Lkotlinx/io/Buffer;)Ljava/lang/String;
-	public static final fun readUtf8 (Lkotlinx/io/Source;)Ljava/lang/String;
-	public static final fun readUtf8 (Lkotlinx/io/Source;J)Ljava/lang/String;
-	public static final fun readUtf8Line (Lkotlinx/io/Source;)Ljava/lang/String;
-	public static final fun readUtf8LineStrict (Lkotlinx/io/Source;J)Ljava/lang/String;
-	public static synthetic fun readUtf8LineStrict$default (Lkotlinx/io/Source;JILjava/lang/Object;)Ljava/lang/String;
-	public static final fun writeUtf8 (Lkotlinx/io/Sink;Ljava/lang/String;II)V
-	public static synthetic fun writeUtf8$default (Lkotlinx/io/Sink;Ljava/lang/String;IIILjava/lang/Object;)V
+	public static final fun readLine (Lkotlinx/io/Source;)Ljava/lang/String;
+	public static final fun readLineStrict (Lkotlinx/io/Source;J)Ljava/lang/String;
+	public static synthetic fun readLineStrict$default (Lkotlinx/io/Source;JILjava/lang/Object;)Ljava/lang/String;
+	public static final fun readString (Lkotlinx/io/Buffer;)Ljava/lang/String;
+	public static final fun readString (Lkotlinx/io/Source;)Ljava/lang/String;
+	public static final fun readString (Lkotlinx/io/Source;J)Ljava/lang/String;
+	public static final fun writeString (Lkotlinx/io/Sink;Ljava/lang/String;II)V
+	public static synthetic fun writeString$default (Lkotlinx/io/Sink;Ljava/lang/String;IIILjava/lang/Object;)V
 }
 
 public final class kotlinx/io/files/Path {
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 850c033e..02cb605b 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -24,7 +24,7 @@ kotlin {
         nodejs {
             testTask {
                 useMocha {
-                    timeout = "30s"
+                    timeout = "300s"
                 }
             }
         }
@@ -32,7 +32,7 @@ kotlin {
             testTask {
                 filter.setExcludePatterns("*SmokeFileTest*")
                 useMocha {
-                    timeout = "30s"
+                    timeout = "300s"
                 }
             }
         }
@@ -40,7 +40,11 @@ kotlin {
 
     configureNativePlatforms()
     sourceSets {
-        val commonMain by getting
+        val commonMain by getting {
+            dependencies {
+                implementation(project(":kotlinx-io-bytestring"))
+            }
+        }
         val commonTest by getting {
             dependencies {
                 implementation(kotlin("test"))
diff --git a/core/common/src/Buffer.kt b/core/common/src/Buffer.kt
index 975473ab..12724a16 100644
--- a/core/common/src/Buffer.kt
+++ b/core/common/src/Buffer.kt
@@ -320,11 +320,11 @@ public class Buffer : Source, Sink {
     override fun readAtMostTo(sink: ByteArray, startIndex: Int, endIndex: Int): Int {
         checkBounds(sink.size, startIndex, endIndex)
 
-        val s = head ?: return -1
-        val toCopy = minOf(endIndex - startIndex, s.limit - s.pos)
-        s.data.copyInto(
-            destination = sink, destinationOffset = startIndex, startIndex = s.pos, endIndex = s.pos + toCopy
-        )
+    val s = head ?: return -1
+    val toCopy = minOf(endIndex - startIndex, s.limit - s.pos)
+    s.data.copyInto(
+      destination = sink, destinationOffset = startIndex, startIndex = s.pos, endIndex = s.pos + toCopy
+    )
 
         s.pos += toCopy
         size -= toCopy.toLong()
diff --git a/core/common/src/BufferExt.kt b/core/common/src/BufferExt.kt
new file mode 100644
index 00000000..07fd950d
--- /dev/null
+++ b/core/common/src/BufferExt.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+package kotlinx.io
+
+import kotlinx.io.bytestring.ByteString
+import kotlinx.io.bytestring.buildByteString
+
+/**
+ * Creates a byte string containing a copy of all the data from this buffer.
+ *
+ * This call doesn't consume data from the buffer, but instead copies it.
+ */
+public fun Buffer.snapshot(): ByteString {
+    if (size == 0L) return ByteString()
+
+    check(size <= Int.MAX_VALUE) { "Buffer is too long ($size) to be converted into a byte string." }
+
+    return buildByteString(size.toInt()) {
+        var curr = head
+        do {
+            check(curr != null) { "Current segment is null" }
+            append(curr.data, curr.pos, curr.limit)
+            curr = curr.next
+        } while (curr !== head)
+    }
+}
\ No newline at end of file
diff --git a/core/common/src/SinkExt.kt b/core/common/src/SinkExt.kt
index 0527b2e1..f0d5385c 100644
--- a/core/common/src/SinkExt.kt
+++ b/core/common/src/SinkExt.kt
@@ -62,7 +62,7 @@ public fun Sink.writeDecimalLong(long: Long) {
     if (v < 0L) {
         v = -v
         if (v < 0L) { // Only true for Long.MIN_VALUE.
-            writeUtf8("-9223372036854775808")
+            writeString("-9223372036854775808")
             return
         }
         negative = true
diff --git a/core/common/src/SourceExt.kt b/core/common/src/SourceExt.kt
index 59f20ca8..811ce44c 100644
--- a/core/common/src/SourceExt.kt
+++ b/core/common/src/SourceExt.kt
@@ -87,7 +87,7 @@ public fun Source.readDecimalLong(): Long {
                     writeByte(b)
 
                     if (!negative) readByte() // Skip negative sign.
-                    throw NumberFormatException("Number too large: ${readUtf8()}")
+                    throw NumberFormatException("Number too large: ${readString()}")
                 }
             }
             value = value * 10L + digit
@@ -143,7 +143,7 @@ public fun Source.readHexadecimalUnsignedLong(): Long {
             with(Buffer()) {
                 writeHexadecimalUnsignedLong(result)
                 writeByte(b)
-                throw NumberFormatException("Number too large: " + readUtf8())
+                throw NumberFormatException("Number too large: " + readString())
             }
         }
         result = result.shl(4) + bDigit
diff --git a/core/common/src/Utf8.kt b/core/common/src/Utf8.kt
index 8e67c0b8..98f33e01 100644
--- a/core/common/src/Utf8.kt
+++ b/core/common/src/Utf8.kt
@@ -72,7 +72,7 @@ package kotlinx.io
 import kotlinx.io.internal.*
 
 /**
- * Returns the number of bytes used to encode the slice of `string` as UTF-8 when using [Sink.writeUtf8].
+ * Returns the number of bytes used to encode the slice of `string` as UTF-8 when using [Sink.writeString].
  *
  * @param startIndex the index (inclusive) of the first character to encode, `0` by default.
  * @param endIndex the index (exclusive) of the character past the last character to encode, `string.length` by default.
@@ -140,7 +140,7 @@ internal fun Sink.writeUtf8CodePoint(codePoint: Int): Unit =
  * @throws IllegalStateException when the sink is closed.
  */
 @OptIn(DelicateIoApi::class)
-public fun Sink.writeUtf8(string: String, startIndex: Int = 0, endIndex: Int = string.length): Unit =
+public fun Sink.writeString(string: String, startIndex: Int = 0, endIndex: Int = string.length): Unit =
     writeToInternalBuffer { it.commonWriteUtf8(string, startIndex, endIndex) }
 
 /**
@@ -151,7 +151,7 @@ public fun Sink.writeUtf8(string: String, startIndex: Int = 0, endIndex: Int = s
  * @throws IllegalStateException when the source is closed.
  */
 @OptIn(InternalIoApi::class)
-public fun Source.readUtf8(): String {
+public fun Source.readString(): String {
     var req: Long = Segment.SIZE.toLong()
     while (request(req)) {
         req *= 2
@@ -164,7 +164,7 @@ public fun Source.readUtf8(): String {
  *
  * Returns the empty string if this buffer is empty.
  */
-public fun Buffer.readUtf8(): String {
+public fun Buffer.readString(): String {
     return commonReadUtf8(size)
 }
 
@@ -178,7 +178,7 @@ public fun Buffer.readUtf8(): String {
  * @throws IllegalStateException when the source is closed.
  */
 @OptIn(InternalIoApi::class)
-public fun Source.readUtf8(byteCount: Long): String {
+public fun Source.readString(byteCount: Long): String {
     require(byteCount)
     return buffer.commonReadUtf8(byteCount)
 }
@@ -220,7 +220,7 @@ internal fun Buffer.readUtf8CodePoint(): Int {
 }
 
 /**
- * Removes and returns characters up to but not including the next line break. A line break is
+ * Removes and returns UTF-8 encoded 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 returns null. If the source doesn't end with a line break, then
@@ -228,7 +228,7 @@ internal fun Buffer.readUtf8CodePoint(): Int {
  *
  * @throws IllegalStateException when the source is closed.
  */
-public fun Source.readUtf8Line(): String? {
+public fun Source.readLine(): String? {
     if (!request(1)) return null
 
     val peekSource = peek()
@@ -247,13 +247,13 @@ public fun Source.readUtf8Line(): String? {
         }
         offset++
     }
-    val line = readUtf8(offset)
+    val line = readString(offset)
     skip(newlineSize)
     return line
 }
 
 /**
- * Removes and returns characters up to but not including the next line break, throwing
+ * Removes and returns UTF-8 encoded characters up to but not including the next line break, throwing
  * [EOFException] if a line break was not encountered. A line break is either `"\n"` or `"\r\n"`;
  * these characters are not included in the result.
  *
@@ -270,7 +270,7 @@ public fun Source.readUtf8Line(): String? {
  * @throws IllegalStateException when the source is closed.
  * @throws IllegalArgumentException when [limit] is negative.
  */
-public fun Source.readUtf8LineStrict(limit: Long = Long.MAX_VALUE): String {
+public fun Source.readLineStrict(limit: Long = Long.MAX_VALUE): String {
     require(limit >= 0) { "limit ($limit) < 0" }
     require(1)
 
@@ -300,7 +300,7 @@ public fun Source.readUtf8LineStrict(limit: Long = Long.MAX_VALUE): String {
         }
     }
     if (newlineSize == 0L) throw EOFException()
-    val line = readUtf8(offset)
+    val line = readString(offset)
     skip(newlineSize)
     return line
 }
diff --git a/core/jvm/src/SinkExtJvm.kt b/core/jvm/src/SinkExtJvm.kt
index d74bad45..494b5c56 100644
--- a/core/jvm/src/SinkExtJvm.kt
+++ b/core/jvm/src/SinkExtJvm.kt
@@ -41,7 +41,7 @@ import java.nio.charset.Charset
  */
 public fun Sink.writeString(string: String, charset: Charset, startIndex: Int = 0, endIndex: Int = string.length) {
     checkBounds(string.length, startIndex, endIndex)
-    if (charset == Charsets.UTF_8) return writeUtf8(string, startIndex, endIndex)
+    if (charset == Charsets.UTF_8) return writeString(string, startIndex, endIndex)
     val data = string.substring(startIndex, endIndex).toByteArray(charset)
     write(data, 0, data.size)
 }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9d18495a..912ac632 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -15,5 +15,7 @@ rootProject.name = "kotlinx-io"
 
 include(":kotlinx-io-core")
 include(":kotlinx-io-benchmarks")
+include(":kotlinx-io-bytestring")
 project(":kotlinx-io-core").projectDir = file("./core")
 project(":kotlinx-io-benchmarks").projectDir = file("./benchmarks")
+project(":kotlinx-io-bytestring").projectDir = file("./bytestring")


Test patch:
diff --git a/bytestring/Module.md b/bytestring/Module.md
new file mode 100644
index 00000000..9a319376
--- /dev/null
+++ b/bytestring/Module.md
@@ -0,0 +1,3 @@
+# Module kotlinx-io-bytestring
+
+The module provides the [ByteString] - an immutable sequence of bytes, and extensions facilitating work with it.
diff --git a/bytestring/api/kotlinx-io-bytestring.api b/bytestring/api/kotlinx-io-bytestring.api
new file mode 100644
index 00000000..8c054de2
--- /dev/null
+++ b/bytestring/api/kotlinx-io-bytestring.api
@@ -0,0 +1,84 @@
+public final class kotlinx/io/bytestring/ByteString : java/lang/Comparable {
+	public static final field Companion Lkotlinx/io/bytestring/ByteString$Companion;
+	public fun <init> ([BII)V
+	public synthetic fun <init> ([BIIILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public synthetic fun <init> ([BLjava/lang/Object;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public synthetic fun compareTo (Ljava/lang/Object;)I
+	public fun compareTo (Lkotlinx/io/bytestring/ByteString;)I
+	public final fun copyInto ([BIII)V
+	public static synthetic fun copyInto$default (Lkotlinx/io/bytestring/ByteString;[BIIIILjava/lang/Object;)V
+	public fun equals (Ljava/lang/Object;)Z
+	public final fun get (I)B
+	public final fun getBackingArrayReference ()[B
+	public final fun getSize ()I
+	public fun hashCode ()I
+	public final fun substring (II)Lkotlinx/io/bytestring/ByteString;
+	public static synthetic fun substring$default (Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)Lkotlinx/io/bytestring/ByteString;
+	public final fun toByteArray (II)[B
+	public static synthetic fun toByteArray$default (Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)[B
+	public fun toString ()Ljava/lang/String;
+}
+
+public final class kotlinx/io/bytestring/ByteString$Companion {
+}
+
+public final class kotlinx/io/bytestring/ByteStringBuilder {
+	public fun <init> ()V
+	public fun <init> (I)V
+	public synthetic fun <init> (IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun append (B)V
+	public final fun append ([BII)V
+	public static synthetic fun append$default (Lkotlinx/io/bytestring/ByteStringBuilder;[BIIILjava/lang/Object;)V
+	public final fun getCapacity ()I
+	public final fun getSize ()I
+	public final fun toByteString ()Lkotlinx/io/bytestring/ByteString;
+}
+
+public final class kotlinx/io/bytestring/ByteStringBuilderKt {
+	public static final fun append (Lkotlinx/io/bytestring/ByteStringBuilder;Lkotlinx/io/bytestring/ByteString;)V
+	public static final fun append (Lkotlinx/io/bytestring/ByteStringBuilder;[B)V
+	public static final fun append-EK-6454 (Lkotlinx/io/bytestring/ByteStringBuilder;B)V
+	public static final fun buildByteString (ILkotlin/jvm/functions/Function1;)Lkotlinx/io/bytestring/ByteString;
+	public static synthetic fun buildByteString$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/io/bytestring/ByteString;
+}
+
+public final class kotlinx/io/bytestring/ByteStringJvmExtKt {
+	public static final fun decodeToString (Lkotlinx/io/bytestring/ByteString;Ljava/nio/charset/Charset;)Ljava/lang/String;
+	public static final fun encodeToByteString (Ljava/lang/String;Ljava/nio/charset/Charset;)Lkotlinx/io/bytestring/ByteString;
+}
+
+public final class kotlinx/io/bytestring/ByteStringKt {
+	public static final fun ByteString ([B)Lkotlinx/io/bytestring/ByteString;
+	public static final fun contentEquals (Lkotlinx/io/bytestring/ByteString;[B)Z
+	public static final fun decodeToString (Lkotlinx/io/bytestring/ByteString;)Ljava/lang/String;
+	public static final fun encodeToByteString (Ljava/lang/String;)Lkotlinx/io/bytestring/ByteString;
+	public static final fun endsWith (Lkotlinx/io/bytestring/ByteString;Lkotlinx/io/bytestring/ByteString;)Z
+	public static final fun endsWith (Lkotlinx/io/bytestring/ByteString;[B)Z
+	public static final fun getIndices (Lkotlinx/io/bytestring/ByteString;)Lkotlin/ranges/IntRange;
+	public static final fun indexOf (Lkotlinx/io/bytestring/ByteString;BI)I
+	public static final fun indexOf (Lkotlinx/io/bytestring/ByteString;Lkotlinx/io/bytestring/ByteString;I)I
+	public static final fun indexOf (Lkotlinx/io/bytestring/ByteString;[BI)I
+	public static synthetic fun indexOf$default (Lkotlinx/io/bytestring/ByteString;BIILjava/lang/Object;)I
+	public static synthetic fun indexOf$default (Lkotlinx/io/bytestring/ByteString;Lkotlinx/io/bytestring/ByteString;IILjava/lang/Object;)I
+	public static synthetic fun indexOf$default (Lkotlinx/io/bytestring/ByteString;[BIILjava/lang/Object;)I
+	public static final fun isEmpty (Lkotlinx/io/bytestring/ByteString;)Z
+	public static final fun isNotEmpty (Lkotlinx/io/bytestring/ByteString;)Z
+	public static final fun lastIndexOf (Lkotlinx/io/bytestring/ByteString;BI)I
+	public static final fun lastIndexOf (Lkotlinx/io/bytestring/ByteString;Lkotlinx/io/bytestring/ByteString;I)I
+	public static final fun lastIndexOf (Lkotlinx/io/bytestring/ByteString;[BI)I
+	public static synthetic fun lastIndexOf$default (Lkotlinx/io/bytestring/ByteString;BIILjava/lang/Object;)I
+	public static synthetic fun lastIndexOf$default (Lkotlinx/io/bytestring/ByteString;Lkotlinx/io/bytestring/ByteString;IILjava/lang/Object;)I
+	public static synthetic fun lastIndexOf$default (Lkotlinx/io/bytestring/ByteString;[BIILjava/lang/Object;)I
+	public static final fun startsWith (Lkotlinx/io/bytestring/ByteString;Lkotlinx/io/bytestring/ByteString;)Z
+	public static final fun startsWith (Lkotlinx/io/bytestring/ByteString;[B)Z
+}
+
+public abstract interface annotation class kotlinx/io/bytestring/unsafe/UnsafeByteStringApi : java/lang/annotation/Annotation {
+}
+
+public final class kotlinx/io/bytestring/unsafe/UnsafeByteStringOperations {
+	public static final field INSTANCE Lkotlinx/io/bytestring/unsafe/UnsafeByteStringOperations;
+	public final fun withByteArrayUnsafe (Lkotlinx/io/bytestring/ByteString;Lkotlin/jvm/functions/Function1;)V
+	public final fun wrapUnsafe ([B)Lkotlinx/io/bytestring/ByteString;
+}
+
diff --git a/bytestring/build.gradle.kts b/bytestring/build.gradle.kts
new file mode 100644
index 00000000..f81792b0
--- /dev/null
+++ b/bytestring/build.gradle.kts
@@ -0,0 +1,80 @@
+import org.jetbrains.dokka.gradle.DokkaTaskPartial
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
+
+plugins {
+    kotlin("multiplatform")
+    id("org.jetbrains.kotlinx.kover") version "0.7.1"
+    id("org.jetbrains.dokka") version "1.8.20"
+}
+
+kotlin {
+    jvm {
+        withJava()
+        testRuns["test"].executionTask.configure {
+            useJUnitPlatform()
+        }
+    }
+
+    js(IR) {
+        nodejs {
+            testTask {
+                useMocha {
+                    timeout = "30s"
+                }
+            }
+        }
+        browser {
+            testTask {
+                filter.setExcludePatterns("*SmokeFileTest*")
+                useMocha {
+                    timeout = "30s"
+                }
+            }
+        }
+    }
+
+    configureNativePlatforms()
+    sourceSets {
+        val commonMain by getting
+        val commonTest by getting {
+            dependencies {
+                implementation(kotlin("test"))
+            }
+        }
+        val jvmMain by getting
+        val jvmTest by getting
+
+        createSourceSet("nativeMain", parent = commonMain, children = nativeTargets)
+        createSourceSet("nativeTest", parent = commonTest, children = nativeTargets)
+    }
+
+    explicitApi()
+    sourceSets.configureEach {
+        configureSourceSet()
+    }
+}
+
+fun KotlinSourceSet.configureSourceSet() {
+    val srcDir = if (name.endsWith("Main")) "src" else "test"
+    val platform = name.dropLast(4)
+    kotlin.srcDir("$platform/$srcDir")
+    if (name == "jvmMain") {
+        resources.srcDir("$platform/resources")
+    } else if (name == "jvmTest") {
+        resources.srcDir("$platform/test-resources")
+    }
+    languageSettings {
+        progressiveMode = true
+    }
+}
+
+tasks.withType<DokkaTaskPartial>().configureEach {
+    dokkaSourceSets.configureEach {
+        includes.from("Module.md")
+
+        perPackageOption {
+            suppress.set(true)
+            matchingRegex.set(".*unsafe.*")
+        }
+    }
+}
diff --git a/bytestring/common/src/ByteString.kt b/bytestring/common/src/ByteString.kt
new file mode 100644
index 00000000..155ee4fa
--- /dev/null
+++ b/bytestring/common/src/ByteString.kt
@@ -0,0 +1,473 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+/*
+ * 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 kotlinx.io.bytestring
+
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Wraps given [bytes] into a byte string.
+ *
+ * @param bytes a sequence of bytes to be wrapped.
+ */
+public fun ByteString(vararg bytes: Byte): ByteString = if (bytes.isEmpty()) {
+    ByteString.EMPTY
+} else {
+    ByteString.wrap(bytes)
+}
+
+/**
+ * An immutable wrapper around a byte sequence providing [String] like functionality.
+ */
+public class ByteString private constructor(
+    private val data: ByteArray,
+    @Suppress("UNUSED_PARAMETER") dummy: Any?
+) : Comparable<ByteString> {
+    /**
+     * Wraps a copy of [data] subarray starting at [startIndex] and ending at [endIndex] into a byte string.
+     *
+     * @param data the array whose subarray should be copied and wrapped into a byte string.
+     * @param startIndex the start index (inclusive) of a subarray to copy, `0` by default.
+     * @param endIndex the end index (exclusive) of a subarray to copy, `data.size` be default.
+     *
+     * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [data] array indices.
+     * @throws IllegalArgumentException when `startIndex > endIndex`.
+     */
+    public constructor(data: ByteArray, startIndex: Int = 0, endIndex: Int = data.size) :
+            this(data.copyOfRange(startIndex, endIndex), null)
+
+    private var hashCode: Int = 0
+
+    public companion object {
+        /**
+         * An empty ByteString.
+         */
+        internal val EMPTY: ByteString = ByteString(ByteArray(0), null)
+
+        internal fun wrap(byteArray: ByteArray): ByteString = ByteString(byteArray, null)
+
+        private val HEX_DIGITS = "0123456789ABCDEF".toCharArray()
+    }
+
+    /**
+     * Returns size of this ByteString.
+     */
+    public val size: Int
+        get(): Int = data.size
+
+    /**
+     * Returns `true` if [other] is a byte string containing exactly the same byte sequence.
+     *
+     * @param other the other object to compare this byte string for equality to.
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as ByteString
+
+        if (other.data.size != data.size) return false
+        if (other.hashCode != 0 && hashCode != 0 && other.hashCode != hashCode) return false
+        return data.contentEquals(other.data)
+    }
+
+    /**
+     * Returns a hash code based on the content of this byte string.
+     */
+    override fun hashCode(): Int {
+        var hc = hashCode
+        if (hc == 0) {
+            hc = data.contentHashCode()
+            hashCode = hc
+        }
+        return hc
+    }
+
+    /**
+     * Returns a byte at the given index in this byte string.
+     *
+     * @param index the index to retrieve the byte at.
+     *
+     * @throws IndexOutOfBoundsException when [index] is negative or greater or equal to the [size].
+     */
+    public operator fun get(index: Int): Byte {
+        if (index < 0 || index >= size) throw IndexOutOfBoundsException(
+            "index ($index) is out of byte string bounds: [0..$size)"
+        )
+        return data[index]
+    }
+
+    /**
+     * Returns a copy of subsequence starting at [startIndex] and ending at [endIndex] of a byte sequence
+     * wrapped by this byte string.
+     *
+     * @param startIndex the start index (inclusive) of a subsequence to copy, `0` by default.
+     * @param endIndex the end index (exclusive) of a subsequence to copy, [size] be default.
+     *
+     * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of byte string indices.
+     * @throws IllegalArgumentException when `startIndex > endIndex`.
+     */
+    public fun toByteArray(startIndex: Int = 0, endIndex: Int = size): ByteArray {
+        require(startIndex <= endIndex) { "startIndex ($startIndex) > endIndex ($endIndex)" }
+        return data.copyOfRange(startIndex, endIndex)
+    }
+
+    /**
+     * Copies a subsequence starting at [startIndex] and ending at [endIndex] of a byte sequence
+     * wrapped by this byte string and writes it into [destination] array starting at [destinationOffset] offset.
+     *
+     * @param destination the array to copy data into.
+     * @param destinationOffset the offset starting from which data copy should be written to [destination].
+     * @param startIndex the start index (inclusive) of a subsequence to copy, `0` by default.
+     * @param endIndex the end index (exclusive) of a subsequence to copy, [size] be default.
+     *
+     * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of byte string indices.
+     * @throws IndexOutOfBoundsException when the subrange doesn't fit into the [destination] array starting at
+     * the specified [destinationOffset], or when that index is out of the [destination] array indices range.
+     * @throws IllegalArgumentException when `startIndex > endIndex`.
+     */
+    public fun copyInto(
+        destination: ByteArray, destinationOffset: Int = 0,
+        startIndex: Int = 0, endIndex: Int = size
+    ) {
+        require(startIndex <= endIndex) { "startIndex ($startIndex) > endIndex ($endIndex)" }
+        data.copyInto(destination, destinationOffset, startIndex, endIndex)
+    }
+
+    /**
+     * Returns a new byte string wrapping a subsequence of bytes wrapped by this byte string starting from
+     * [startIndex] and ending at [endIndex].
+     *
+     * @param startIndex the start index (inclusive) of a subsequence to copy.
+     * @param endIndex the end index (exclusive) of a subsequence to copy, [size] be default.
+     *
+     * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of byte string indices.
+     * @throws IllegalArgumentException when `startIndex > endIndex`.
+     */
+    public fun substring(startIndex: Int, endIndex: Int = size): ByteString = if (startIndex == endIndex) {
+        EMPTY
+    } else {
+        ByteString(data, startIndex, endIndex)
+    }
+
+    /**
+     * Compares a byte sequence wrapped by this byte string to a byte sequence wrapped by [other]
+     * in lexicographical order.
+     * Byte values are compared as unsigned integers.
+     *
+     * The behavior is similar to [String.compareTo].
+     *
+     * @param other the byte string to compare this string to.
+     */
+    override fun compareTo(other: ByteString): Int {
+        if (other === this) return 0
+        val localData = data
+        val otherData = other.data
+        for (i in 0 until min(size, other.size)) {
+            val cmp = localData[i].toUByte().compareTo(otherData[i].toUByte())
+            if (cmp != 0) return cmp
+        }
+
+        return size.compareTo(other.size)
+    }
+
+    /**
+     * Returns a string representation of this byte string. A string representation consists of [size] and
+     * a hexadecimal-encoded string of a byte sequence wrapped by this byte string.
+     *
+     * The string representation has the following format `ByteString(size=3 hex=ABCDEF)`,
+     * for empty strings it's always `ByteString(size=0)`.
+     *
+     * Note that a string representation includes the whole byte string content encoded.
+     * Due to limitations exposed for the maximum string length, an attempt to return a string representation
+     * of too long byte string may fail.
+     */
+    override fun toString(): String {
+        if (isEmpty()) {
+            return "ByteString(size=0)"
+        }
+        // format: "ByteString(size=XXX hex=YYYY)"
+        val sizeStr = size.toString()
+        val len = 22 + sizeStr.length + size * 2
+        return with(StringBuilder(len)) {
+            append("ByteString(size=")
+            append(sizeStr)
+            append(" hex=")
+            val localData = data
+            for (i in 0 until size) {
+                val b = localData[i].toInt()
+                append(HEX_DIGITS[(b ushr 4) and 0xf])
+                append(HEX_DIGITS[b and 0xf])
+            }
+            append(')')
+        }.toString()
+    }
+
+    /**
+     * Returns a reference to the underlying array.
+     *
+     * These methods return reference to the underlying array, not to its copy.
+     * Consider using [toByteArray] if it's impossible to guarantee that the array won't be modified.
+     */
+    @PublishedApi
+    internal fun getBackingArrayReference(): ByteArray = data
+}
+
+/**
+ * Returns the range of valid byte indices for this byte string.
+ */
+public val ByteString.indices: IntRange
+    get() = 0 until size
+
+/**
+ * Returns the index within this byte string of the first occurrence of the specified [byte],
+ * starting from the specified [startIndex].
+ * If the [byte] not found, `-1` is returned.
+ *
+ * Behavior of this method is compatible with [CharSequence.indexOf].
+ *
+ * @param byte the value to search for.
+ * @param startIndex the index (inclusive) starting from which the [byte] should be searched.
+ */
+public fun ByteString.indexOf(byte: Byte, startIndex: Int = 0): Int {
+    val localData = getBackingArrayReference()
+    for (i in max(startIndex, 0) until size) {
+        if (localData[i] == byte) {
+            return i
+        }
+    }
+    return -1
+}
+
+/**
+ * Returns the index within this byte string of the first occurrence of the specified [byteString],
+ * starting from the specified [startIndex].
+ * If the [byteString] not found, `-1` is returned.
+ *
+ * Behavior of this method is compatible with [CharSequence.indexOf].
+ *
+ * @param byteString the value to search for.
+ * @param startIndex the index (inclusive) starting from which the [byteString] should be searched.
+ */
+public fun ByteString.indexOf(byteString: ByteString, startIndex: Int = 0): Int {
+    if (byteString.isEmpty()) return max(min(startIndex, size), 0)
+    val localData = getBackingArrayReference()
+    val firstByte = byteString[0]
+    for (i in max(startIndex, 0)..size - byteString.size) {
+        if (localData[i] == firstByte && rangeEquals(i, byteString)) {
+            return i
+        }
+    }
+    return -1
+}
+
+/**
+ * Returns the index within this byte string of the first occurrence of the specified [byteArray],
+ * starting from the specified [startIndex].
+ * If the [byteArray] not found, `-1` is returned.
+ *
+ * Behavior of this method is compatible with [CharSequence.indexOf].
+ *
+ * @param byteArray the value to search for.
+ * @param startIndex the index (inclusive) starting from which the [byteArray] should be searched.
+ */
+public fun ByteString.indexOf(byteArray: ByteArray, startIndex: Int = 0): Int {
+    if (byteArray.isEmpty()) return max(min(startIndex, size), 0)
+    val localData = getBackingArrayReference()
+    val firstByte = byteArray[0]
+    for (i in max(0, startIndex)..size - byteArray.size) {
+        if (localData[i] == firstByte && rangeEquals(i, byteArray)) {
+            return i
+        }
+    }
+    return -1
+}
+
+/**
+ * Returns the index within this char sequence of the last occurrence of the specified [byte],
+ * starting from the specified [startIndex].
+ * If the [byte] not found, `-1` is returned.
+ *
+ * Behavior of this method is compatible with [CharSequence.lastIndexOf].
+ *
+ * @param byte the value to search for.
+ * @param startIndex the index (inclusive) starting from which the [byte] should be searched.
+ */
+public fun ByteString.lastIndexOf(byte: Byte, startIndex: Int = 0): Int {
+    val localData = getBackingArrayReference()
+    for (i in size - 1 downTo max(0, startIndex)) {
+        if (localData[i] == byte) {
+            return i
+        }
+    }
+    return -1
+}
+
+/**
+ * Returns the index within this char sequence of the last occurrence of the specified [byteString],
+ * starting from the specified [startIndex].
+ * If the [byteString] not found, `-1` is returned.
+ *
+ * Behavior of this method is compatible with [CharSequence.lastIndexOf].
+ *
+ * @param byteString the value to search for.
+ * @param startIndex the index (inclusive) starting from which the [byteString] should be searched.
+ */
+public fun ByteString.lastIndexOf(byteString: ByteString, startIndex: Int = 0): Int {
+    if (byteString.isEmpty()) return size
+    for (idx in (size - byteString.size) downTo max(0, startIndex)) {
+        if (rangeEquals(idx, byteString, 0)) {
+            return idx
+        }
+    }
+    return -1
+}
+
+/**
+ * Returns the index within this char sequence of the last occurrence of the specified [byteArray],
+ * starting from the specified [startIndex].
+ * If the [byteArray] not found, `-1` is returned.
+ *
+ * Behavior of this method is compatible with [CharSequence.lastIndexOf].
+ *
+ * @param byteArray the value to search for.
+ * @param startIndex the index (inclusive) starting from which the [byteArray] should be searched.
+ */
+public fun ByteString.lastIndexOf(byteArray: ByteArray, startIndex: Int = 0): Int {
+    if (byteArray.isEmpty()) return size
+    for (idx in (size - byteArray.size) downTo max(0, startIndex)) {
+        if (rangeEquals(idx, byteArray, 0)) {
+            return idx
+        }
+    }
+    return -1
+}
+
+/**
+ * Returns true if this byte string starts with the prefix specified by the [byteArray].
+ *
+ * Behavior of this method is compatible with [CharSequence.startsWith].
+ *
+ * @param byteArray the prefix to check for.
+ */
+public fun ByteString.startsWith(byteArray: ByteArray): Boolean = when {
+    byteArray.size > size -> false
+    else -> rangeEquals(0, byteArray)
+}
+
+/**
+ * Returns true if this byte string starts with the prefix specified by the [byteString].
+ *
+ * Behavior of this method is compatible with [CharSequence.startsWith].
+ *
+ * @param byteString the prefix to check for.
+ */
+public fun ByteString.startsWith(byteString: ByteString): Boolean = when {
+    byteString.size > size -> false
+    byteString.size == size -> equals(byteString)
+    else -> rangeEquals(0, byteString)
+}
+
+/**
+ * Returns true if this byte string ends with the suffix specified by the [byteArray].
+ *
+ * Behavior of this method is compatible with [CharSequence.endsWith].
+ *
+ * @param byteArray the suffix to check for.
+ */
+public fun ByteString.endsWith(byteArray: ByteArray): Boolean = when {
+    byteArray.size > size -> false
+    else -> rangeEquals(size - byteArray.size, byteArray)
+}
+
+/**
+ * Returns true if this byte string ends with the suffix specified by the [byteString].
+ *
+ * Behavior of this method is compatible with [CharSequence.endsWith].
+ *
+ * @param byteString the suffix to check for.
+ */
+public fun ByteString.endsWith(byteString: ByteString): Boolean = when {
+    byteString.size > size -> false
+    byteString.size == size -> equals(byteString)
+    else -> rangeEquals(size - byteString.size, byteString)
+}
+
+private fun ByteString.rangeEquals(
+    offset: Int, other: ByteString, otherOffset: Int = 0,
+    byteCount: Int = other.size - otherOffset
+): Boolean {
+    val localData = getBackingArrayReference()
+    val otherData = other.getBackingArrayReference()
+    for (i in 0 until byteCount) {
+        if (localData[offset + i] != otherData[otherOffset + i]) {
+            return false
+        }
+    }
+    return true
+}
+
+private fun ByteString.rangeEquals(
+    offset: Int, other: ByteArray, otherOffset: Int = 0,
+    byteCount: Int = other.size - otherOffset
+): Boolean {
+    val localData = getBackingArrayReference()
+    for (i in 0 until byteCount) {
+        if (localData[offset + i] != other[otherOffset + i]) {
+            return false
+        }
+    }
+    return true
+}
+
+/**
+ * Returns `true` if this byte string is empty.
+ */
+public fun ByteString.isEmpty(): Boolean = size == 0
+
+/**
+ * Returns `true` if this byte string is not empty.
+ */
+public fun ByteString.isNotEmpty(): Boolean = !isEmpty()
+
+/**
+ * Decodes content of a byte string into a string using UTF-8 encoding.
+ */
+public fun ByteString.decodeToString(): String {
+    return getBackingArrayReference().decodeToString()
+}
+
+/**
+ * Encodes a string into a byte sequence using UTF8-encoding and wraps it into a byte string.
+ */
+public fun String.encodeToByteString(): ByteString {
+    return ByteString.wrap(encodeToByteArray())
+}
+
+/**
+ * Returns `true` if the content of this byte string equals to the [array].
+ *
+ * @param array the array to test this byte string's content against.
+ */
+public fun ByteString.contentEquals(array: ByteArray): Boolean {
+    return getBackingArrayReference().contentEquals(array)
+}
diff --git a/bytestring/common/src/ByteStringBuilder.kt b/bytestring/common/src/ByteStringBuilder.kt
new file mode 100644
index 00000000..3d62964e
--- /dev/null
+++ b/bytestring/common/src/ByteStringBuilder.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+package kotlinx.io.bytestring
+
+import kotlin.math.max
+
+/**
+ * A helper class facilitating [ByteString] construction.
+ *
+ * A builder is characterized by the [capacity] - the number of bytes that an instance of builder
+ * can receive before extending an underlying byte sequence, and [size] - the number of bytes being written
+ * to the builder.
+ *
+ * The builder avoids additional copies and allocations when `size == capacity` when [toByteString] called,
+ * thus it's recommended to specify expected [ByteString] size as `initialCapacity` when creating a builder.
+ *
+ * When a builder runs out of available capacity, a new byte sequence with extended capacity
+ * will be allocated and previously written data will be copied into it.
+ *
+ * @param initialCapacity the initial size of an underlying byte sequence.
+ */
+public class ByteStringBuilder(initialCapacity: Int = 0) {
+    private var buffer = ByteArray(initialCapacity)
+    private var offset: Int = 0
+
+    /**
+     * The number of bytes being written to this builder.
+     */
+    public val size: Int
+        get() = offset
+
+    /**
+     * The number of bytes this builder can store without an extension of an internal buffer.
+     */
+    public val capacity: Int
+        get() = buffer.size
+
+    /**
+     * Returns a new [ByteString] wrapping all bytes written to this builder.
+     *
+     * There will be no additional allocations or copying of data when `size == capacity`.
+     */
+    public fun toByteString(): ByteString {
+        if (size == 0) {
+            return ByteString()
+        }
+        if (buffer.size == size) {
+            return ByteString.wrap(buffer)
+        }
+        return ByteString(buffer, 0, size)
+    }
+
+    /**
+     * Append a single byte to this builder.
+     *
+     * @param byte the byte to append.
+     */
+    public fun append(byte: Byte) {
+        ensureCapacity(size + 1)
+        buffer[offset++] = byte
+    }
+
+    /**
+     * Appends a subarray of [array] starting at [startIndex] and ending at [endIndex] to this builder.
+     *
+     * @param array the array whose subarray should be appended.
+     * @param startIndex the first index (inclusive) to copy data from the [array].
+     * @param endIndex the last index (exclusive) to copy data from the [array]
+     *
+     * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [array] array indices.
+     * @throws IllegalArgumentException when `startIndex > endIndex`.
+     */
+    public fun append(array: ByteArray, startIndex: Int = 0, endIndex: Int = array.size) {
+        require(startIndex <= endIndex) { "startIndex ($startIndex) > endIndex ($endIndex)" }
+        if (startIndex < 0 || endIndex > array.size) {
+            throw IndexOutOfBoundsException("startIndex ($startIndex) and endIndex ($endIndex) represents " +
+                    "an interval out of array's bounds [0..${array.size}).")
+        }
+        ensureCapacity(offset + endIndex - startIndex)
+
+        array.copyInto(buffer, offset, startIndex, endIndex)
+        offset += endIndex - startIndex
+    }
+
+    private fun ensureCapacity(requiredCapacity: Int) {
+        if (buffer.size >= requiredCapacity) {
+            return
+        }
+
+        var desiredSize = if (buffer.isEmpty()) 16 else (buffer.size * 1.5).toInt()
+        desiredSize = max(desiredSize, requiredCapacity)
+        val newBuffer = ByteArray(desiredSize)
+        buffer.copyInto(newBuffer)
+        buffer = newBuffer
+    }
+}
+
+/**
+ * Appends unsigned byte to this builder.
+ */
+public fun ByteStringBuilder.append(byte: UByte): Unit = append(byte.toByte())
+
+/**
+ * Appends a byte string to this builder.
+ */
+public fun ByteStringBuilder.append(byteString: ByteString) {
+    append(byteString.getBackingArrayReference())
+}
+
+/**
+ * Appends bytes to this builder.
+ */
+public fun ByteStringBuilder.append(vararg bytes: Byte): Unit = append(bytes)
+
+
+/**
+ * Builds new byte string by populating newly created [ByteStringBuilder] initialized with the given [capacity]
+ * using provided [builderAction] and then converting it to [ByteString].
+ */
+public inline fun buildByteString(capacity: Int = 0, builderAction: ByteStringBuilder.() -> Unit): ByteString {
+    return ByteStringBuilder(capacity).apply(builderAction).toByteString()
+}
diff --git a/bytestring/common/src/unsafe/Annotations.kt b/bytestring/common/src/unsafe/Annotations.kt
new file mode 100644
index 00000000..41ece762
--- /dev/null
+++ b/bytestring/common/src/unsafe/Annotations.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+package kotlinx.io.bytestring.unsafe
+
+/**
+ * Marks declarations whose usage may brake some ByteString invariants.
+ *
+ * Consider using other APIs instead when possible.
+ * Otherwise, make sure to read documentation describing an unsafe API.
+ */
+@MustBeDocumented
+@Retention(AnnotationRetention.BINARY)
+@RequiresOptIn(
+    level = RequiresOptIn.Level.ERROR,
+    message = "This is a unsafe API and its use may corrupt the data stored in a byte string. " +
+            "Make sure you fully read and understand documentation of the declaration that is marked as an unsafe API."
+)
+public annotation class UnsafeByteStringApi
diff --git a/bytestring/common/src/unsafe/UnsafeByteStringOperations.kt b/bytestring/common/src/unsafe/UnsafeByteStringOperations.kt
new file mode 100644
index 00000000..fdc89e3b
--- /dev/null
+++ b/bytestring/common/src/unsafe/UnsafeByteStringOperations.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+package kotlinx.io.bytestring.unsafe
+
+import kotlinx.io.bytestring.ByteString
+
+/**
+ * Collection of helper functions providing unsafe access to the [ByteString]'s underlying byte sequence or allowing
+ * to wrap byte arrays into [ByteString] without copying the array.
+ *
+ * These functions are provided for performance sensitive cases where it is known that the data accessed
+ * in an unsafe manner won't be modified. Modification of the data backing byte strings may lead to unpredicted
+ * consequences in the code using the byte string and should be avoided at all costs.
+ */
+@UnsafeByteStringApi
+public object UnsafeByteStringOperations {
+    /**
+     * Creates a new byte string by wrapping [array] without copying it.
+     * Make sure that the wrapped array won't be modified during the lifespan of the returned byte string.
+     *
+     * @param array the array to wrap into the byte string.
+     */
+    public fun wrapUnsafe(array: ByteArray): ByteString = ByteString.wrap(array)
+
+    /**
+     * Applies [block] to a reference to the underlying array.
+     *
+     * This method invokes [block] on a reference to the underlying array, not to its copy.
+     * Consider using [ByteString.toByteArray] if it's impossible to guarantee that the array won't be modified.
+     */
+    public inline fun withByteArrayUnsafe(byteString: ByteString, block: (ByteArray) -> Unit) {
+        block(byteString.getBackingArrayReference())
+    }
+}
diff --git a/bytestring/common/test/ByteStringBuilderTest.kt b/bytestring/common/test/ByteStringBuilderTest.kt
new file mode 100644
index 00000000..2fbb0540
--- /dev/null
+++ b/bytestring/common/test/ByteStringBuilderTest.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+package kotlinx.io.bytestring
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+class ByteStringBuilderTest {
+    @Test
+    fun emptyString() {
+        assertTrue(ByteStringBuilder().toByteString().isEmpty())
+        assertTrue(ByteStringBuilder(1024).toByteString().isEmpty())
+    }
+
+    @Test
+    fun appendByte() {
+        val builder = ByteStringBuilder()
+        with(builder) {
+            append(1)
+            append(2)
+            append(3)
+        }
+        assertEquals(ByteString(1, 2, 3), builder.toByteString())
+    }
+
+    @Test
+    fun appendBytes() {
+        assertEquals(ByteString(1, 2, 3), buildByteString { append(1, 2, 3) })
+    }
+
+    @Test
+    fun appendUByte() {
+        val builder = ByteStringBuilder()
+        with(builder) {
+            append(0x80U)
+            append(0x81U)
+            append(0x82U)
+        }
+        assertEquals(ByteString(0x80U.toByte(), 0x81U.toByte(), 0x82U.toByte()), builder.toByteString())
+    }
+
+    @Test
+    fun appendArray() {
+        with(ByteStringBuilder()) {
+            append(byteArrayOf(1, 2, 3, 4))
+            assertEquals(ByteString(1, 2, 3, 4), toByteString())
+        }
+
+        with(ByteStringBuilder()) {
+            append(byteArrayOf(1, 2, 3, 4), startIndex = 2)
+            assertEquals(ByteString(3, 4), toByteString())
+        }
+
+        with(ByteStringBuilder()) {
+            append(byteArrayOf(1, 2, 3, 4), endIndex = 2)
+            assertEquals(ByteString(1, 2), toByteString())
+        }
+
+        with(ByteStringBuilder()) {
+            append(byteArrayOf(1, 2, 3, 4), startIndex = 1, endIndex = 3)
+            assertEquals(ByteString(2, 3), toByteString())
+        }
+
+        with(ByteStringBuilder()) {
+            append(byteArrayOf(1, 2, 3, 4), startIndex = 1, endIndex = 1)
+            assertEquals(ByteString(), toByteString())
+        }
+    }
+
+    @Test
+    fun testAppendByteArrayWithInvalidIndices() {
+        val builder = ByteStringBuilder()
+        val array = ByteArray(10)
+        assertFailsWith<IllegalArgumentException> { builder.append(array, 2, 0) }
+        assertFailsWith<IndexOutOfBoundsException> { builder.append(array, -1, 2) }
+        assertFailsWith<IndexOutOfBoundsException> { builder.append(array, 0, 1000) }
+        assertFailsWith<IndexOutOfBoundsException> { builder.append(array, 1000, 1001) }
+    }
+
+    @Test
+    fun appendByteString() {
+        val builder = ByteStringBuilder()
+        builder.append(ByteString(1, 2, 3, 4))
+        assertEquals(ByteString(1, 2, 3, 4), builder.toByteString())
+    }
+
+    @Test
+    fun appendMultipleValues() {
+        val string = with(ByteStringBuilder()) {
+            append(42)
+            append(ByteArray(10) { it.toByte() })
+            append(42U)
+            append(ByteString(10, 5, 57))
+            toByteString()
+        }
+
+        assertEquals(ByteString(42, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 42, 10, 5, 57), string)
+    }
+
+    @Test
+    fun resizeMultipleTimes() {
+        val builder = ByteStringBuilder()
+        builder.append(ByteArray(1))
+        builder.append(ByteArray(32))
+        builder.append(ByteArray(120))
+        builder.append(ByteArray(1024))
+
+        assertEquals(ByteString(ByteArray(1 + 32 + 120 + 1024)), builder.toByteString())
+    }
+
+    @Test
+    fun testSize() {
+        val builder = ByteStringBuilder()
+        assertEquals(0, builder.size)
+        builder.append(1)
+        assertEquals(1, builder.size)
+        builder.append(ByteArray(33))
+        assertEquals(34, builder.size)
+    }
+
+    @Test
+    fun testCapacity() {
+        assertEquals(0, ByteStringBuilder().capacity)
+        assertEquals(10, ByteStringBuilder(10).capacity)
+
+        with(ByteStringBuilder()) {
+            append(1)
+            assertTrue(capacity >= 1)
+            append(ByteArray(1024))
+            assertTrue(capacity >= 1025)
+        }
+    }
+
+    @Test
+    fun createMultipleByteStrings() {
+        val builder = ByteStringBuilder()
+        builder.append(1)
+        val str0 = builder.toByteString()
+        assertEquals(ByteString(1), str0)
+        assertEquals(ByteString(1), builder.toByteString())
+        builder.append(2)
+        assertEquals(ByteString(1, 2), builder.toByteString())
+        assertEquals(ByteString(1), str0)
+    }
+}
diff --git a/bytestring/common/test/ByteStringTest.kt b/bytestring/common/test/ByteStringTest.kt
new file mode 100644
index 00000000..6048d741
--- /dev/null
+++ b/bytestring/common/test/ByteStringTest.kt
@@ -0,0 +1,413 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+package kotlinx.io.bytestring
+
+import kotlin.test.*
+
+class ByteStringTest {
+    @Test
+    fun get() {
+        val actual = ByteString("abc".encodeToByteArray())
+        assertEquals(3, actual.size)
+        assertEquals(actual[0], 'a'.code.toByte())
+        assertEquals(actual[1], 'b'.code.toByte())
+        assertEquals(actual[2], 'c'.code.toByte())
+    }
+
+    @Test
+    fun getWithInvalidIndex() {
+        val str = ByteString(0, 1, 2)
+        assertFailsWith<IndexOutOfBoundsException> { str[-1] }
+        assertFailsWith<IndexOutOfBoundsException> { str[3] }
+    }
+
+    @Test
+    fun equalsAndHashCode() {
+        with(ByteString(1, 2, 3)) { checkEqualsAndHashCodeAreSame(this, this) }
+        checkEqualsAndHashCodeAreSame(ByteString(), ByteString(byteArrayOf()))
+        checkEqualsAndHashCodeAreSame(ByteString(1, 2, 3), ByteString(1, 2, 3))
+
+        assertNotEquals(ByteString(1, 2, 3), ByteString(3, 2, 1))
+        assertNotEquals(ByteString(1, 2, 3).hashCode(), ByteString(3, 2, 1).hashCode())
+
+        assertNotEquals(ByteString(1, 2, 3, 4), ByteString(1, 2, 3))
+
+        val str1 = ByteString(1, 2, 3)
+        val str2 = ByteString(2, 3, 4)
+        // force hashCode computation
+        assertNotEquals(str1.hashCode(), str2.hashCode())
+        assertNotEquals(str1, str2)
+
+        assertFalse(ByteString().equals(null))
+        assertFalse(ByteString().equals(byteArrayOf(1, 2, 3)))
+    }
+
+    private fun checkEqualsAndHashCodeAreSame(first: ByteString, second: ByteString) {
+        assertEquals(first, second)
+        assertEquals(first.hashCode(), second.hashCode())
+    }
+
+    @Test
+    fun toByteArray() {
+        val str = ByteString(1, 2, 3, 4, 5, 6)
+        assertContentEquals(byteArrayOf(1, 2, 3, 4, 5, 6), str.toByteArray())
+        assertContentEquals(byteArrayOf(), str.toByteArray(0, 0))
+        assertContentEquals(byteArrayOf(1, 2, 3), str.toByteArray(endIndex = 3))
+        assertContentEquals(byteArrayOf(4, 5, 6), str.toByteArray(startIndex = 3))
+        assertContentEquals(byteArrayOf(2, 3, 4), str.toByteArray(startIndex = 1, endIndex = 4))
+    }
+
+    @Test
+    fun toByteArrayWithInvalidIndex() {
+        val str = ByteString(1, 2, 3)
+        assertFailsWith<IndexOutOfBoundsException> { str.toByteArray(-1, 1) }
+        assertFailsWith<IndexOutOfBoundsException> { str.toByteArray(1, 4) }
+        assertFailsWith<IndexOutOfBoundsException> { str.toByteArray(-1, 4) }
+        assertFailsWith<IllegalArgumentException> { str.toByteArray(2, 0) }
+    }
+
+    @Test
+    fun copyTo() {
+        val str = ByteString(1, 2, 3, 4, 5, 6)
+        val dest = ByteArray(10)
+
+        str.copyInto(dest)
+        assertContentEquals(byteArrayOf(1, 2, 3, 4, 5, 6, 0, 0, 0, 0), dest)
+
+        dest.fill(0)
+        str.copyInto(dest, 2)
+        assertContentEquals(byteArrayOf(0, 0, 1, 2, 3, 4, 5, 6, 0, 0), dest)
+
+        dest.fill(0)
+        str.copyInto(dest, destinationOffset = 0, startIndex = 1)
+        assertContentEquals(byteArrayOf(2, 3, 4, 5, 6, 0, 0, 0, 0, 0), dest)
+
+        dest.fill(0)
+        str.copyInto(dest, destinationOffset = 0, endIndex = 3)
+        assertContentEquals(byteArrayOf(1, 2, 3, 0, 0, 0, 0, 0, 0, 0), dest)
+
+        dest.fill(0)
+        str.copyInto(dest, destinationOffset = 0, startIndex = 3, endIndex = 5)
+        assertContentEquals(byteArrayOf(4, 5, 0, 0, 0, 0, 0, 0, 0, 0), dest)
+
+        dest.fill(0)
+        str.copyInto(dest, destinationOffset = 5, endIndex = 5)
+        assertContentEquals(byteArrayOf(0, 0, 0, 0, 0, 1, 2, 3, 4, 5), dest)
+
+        dest.fill(0)
+        str.copyInto(dest, startIndex = 3, endIndex = 3)
+        assertContentEquals(ByteArray(10), dest)
+    }
+
+    @Test
+    fun copyToWithInvalidArguments() {
+        val str = ByteString(1, 2, 3)
+        val dest = ByteArray(10)
+
+        assertFailsWith<IllegalArgumentException> { str.copyInto(dest, 0, startIndex = 1, endIndex = 0) }
+        assertFailsWith<IndexOutOfBoundsException> { str.copyInto(dest, 9) }
+        assertFailsWith<IndexOutOfBoundsException> { str.copyInto(dest, -1) }
+        assertFailsWith<IndexOutOfBoundsException> { str.copyInto(dest, 0, startIndex = -1) }
+        assertFailsWith<IndexOutOfBoundsException> { str.copyInto(dest, 0, endIndex = 5) }
+    }
+
+    @Test
+    fun substring() {
+        val str = ByteString(1, 2, 3, 4, 5)
+
+        assertEquals(ByteString(), str.substring(0, 0))
+        assertEquals(ByteString(1, 2, 3), str.substring(startIndex = 0, endIndex = 3))
+        assertEquals(ByteString(3, 4, 5), str.substring(startIndex = 2))
+        assertEquals(ByteString(2, 3, 4), str.substring(startIndex = 1, endIndex = 4))
+    }
+
+    @Test
+    fun substringWithInvalidArgs() {
+        val str = ByteString(1, 2, 3)
+
+        assertFailsWith<IllegalArgumentException> { str.substring(2, 1) }
+        assertFailsWith<IndexOutOfBoundsException> { str.substring(-1) }
+        assertFailsWith<IndexOutOfBoundsException> { str.substring(0, 10) }
+        assertFailsWith<IndexOutOfBoundsException> { str.substring(-10, 10) }
+    }
+
+    @Test
+    fun compareTo() {
+        assertEquals(0, ByteString().compareTo(ByteString()))
+        assertEquals(0, ByteString(1, 2, 3).compareTo(ByteString(1, 2, 3)))
+        assertEquals(-1, ByteString(1, 2).compareTo(ByteString(1, 2, 3)))
+        assertEquals(-1, ByteString(0, 1, 2).compareTo(ByteString(0, 1, 3)))
+        assertEquals(1, ByteString(1, 2, 3).compareTo(ByteString(1, 2)))
+        assertEquals(1, ByteString(1, 2, 3).compareTo(ByteString(0, 1, 2)))
+        assertEquals(1, ByteString(0xFF.toByte()).compareTo(ByteString(0)))
+        assertEquals(-1, ByteString(1).compareTo(ByteString(0x81.toByte())))
+    }
+
+    @Test
+    fun size() {
+        assertEquals(0, ByteString().size)
+        assertEquals(1, ByteString(0).size)
+        assertEquals(12345, ByteString(ByteArray(12345)).size)
+    }
+
+    @Test
+    fun indices() {
+        assertEquals(0 until 10, ByteString(ByteArray(10)).indices)
+        assertTrue(ByteString().indices.isEmpty())
+    }
+
+    @Test
+    fun isEmpty() {
+        assertTrue(ByteString().isEmpty())
+        assertTrue(ByteString(byteArrayOf()).isEmpty())
+        assertFalse(ByteString(byteArrayOf(0)).isEmpty())
+    }
+
+    @Test
+    fun isNotEmpty() {
+        assertFalse(ByteString().isNotEmpty())
+        assertFalse(ByteString(byteArrayOf()).isNotEmpty())
+        assertTrue(ByteString(byteArrayOf(0)).isNotEmpty())
+    }
+
+    @Test
+    fun indexOfByte() {
+        val str = ByteString(1, 2, 3, 4)
+        for (idx in str.indices) {
+            assertEquals(idx, str.indexOf(str[idx]))
+        }
+
+        assertEquals(-1, str.indexOf(0))
+        assertEquals(-1, str.indexOf(1, 1))
+        assertEquals(-1, str.indexOf(4, 4))
+        assertEquals(0, str.indexOf(1, -10))
+        assertEquals(1, ByteString(0, 1, 1, 1).indexOf(1))
+
+        assertEquals(-1, ByteString().indexOf(0))
+        assertEquals(-1, ByteString().indexOf(0, 100500))
+        assertEquals(-1, str.indexOf(1, 100500))
+    }
+
+    @Test
+    fun indexOfByteArray() {
+        val str = ByteString(1, 2, 3, 4, 5)
+
+        assertEquals(0, str.indexOf(byteArrayOf(1, 2, 3, 4, 5)))
+        assertEquals(0, str.indexOf(byteArrayOf(1, 2, 3)))
+        assertEquals(0, str.indexOf(byteArrayOf(1)))
+
+        assertEquals(2, str.indexOf(byteArrayOf(3, 4, 5)))
+        assertEquals(-1, str.indexOf(byteArrayOf(3, 4, 5, 6)))
+        assertEquals(0, str.indexOf(byteArrayOf()))
+        assertEquals(-1, str.indexOf(byteArrayOf(-1)))
+
+        assertEquals(-1, str.indexOf(byteArrayOf(1, 2, 3, 4, 5), 1))
+        assertEquals(3, str.indexOf(byteArrayOf(4, 5), 3))
+
+        assertEquals(0, str.indexOf(byteArrayOf(1, 2, 3), -1000))
+        assertEquals(1, str.indexOf(byteArrayOf(2, 3), -1))
+
+        assertEquals(1, ByteString(0, 1, 0, 1, 0, 1).indexOf(byteArrayOf(1, 0)))
+
+        assertEquals(0, ByteString().indexOf(byteArrayOf()))
+        assertEquals(0, ByteString().indexOf(byteArrayOf(), -100500))
+        assertEquals(0, ByteString().indexOf(byteArrayOf(), 100500))
+        assertEquals(-1, str.indexOf(byteArrayOf(1, 2, 3), 100500))
+        assertEquals(-1, ByteString().indexOf(byteArrayOf(1, 2, 3, 4, 5)))
+        assertEquals(-1, str.indexOf(byteArrayOf(2, 3, 5)))
+    }
+
+    @Test
+    fun indexOfByteString() {
+        val str = ByteString(1, 2, 3, 4, 5)
+
+        assertEquals(0, str.indexOf(ByteString(1, 2, 3, 4, 5)))
+        assertEquals(0, str.indexOf(ByteString(1, 2, 3)))
+        assertEquals(0, str.indexOf(ByteString(1)))
+        assertEquals(2, str.indexOf(ByteString(3, 4, 5)))
+        assertEquals(-1, str.indexOf(ByteString(3, 4, 5, 6)))
+        assertEquals(0, str.indexOf(ByteString()))
+        assertEquals(-1, str.indexOf(ByteString(-1)))
+        assertEquals(-1, str.indexOf(ByteString(1, 2, 3, 4, 5), 1))
+        assertEquals(3, str.indexOf(ByteString(4, 5), 3))
+        assertEquals(0, str.indexOf(ByteString(1, 2, 3), -1000))
+        assertEquals(1, str.indexOf(ByteString(2, 3), -1))
+        assertEquals(1, ByteString(0, 1, 0, 1, 0, 1).indexOf(ByteString(1, 0)))
+        assertEquals(0, ByteString().indexOf(ByteString()))
+        assertEquals(0, ByteString().indexOf(ByteString(), -100500))
+        assertEquals(0, ByteString().indexOf(ByteString(), 100500))
+        assertEquals(-1, str.indexOf(ByteString(1, 2, 3), 100500))
+        assertEquals(-1, ByteString().indexOf(ByteString(1, 2, 3, 4, 5)))
+        assertEquals(-1, str.indexOf(ByteString(2, 3, 5)))
+    }
+
+    @Test
+    fun lastIndexOfByte() {
+        val str = ByteString(1, 2, 3, 4)
+        for (idx in str.indices) {
+            assertEquals(idx, str.lastIndexOf(str[idx]))
+        }
+
+        assertEquals(-1, str.lastIndexOf(0))
+        assertEquals(-1, str.lastIndexOf(1, 1))
+        assertEquals(-1, str.lastIndexOf(4, 4))
+        assertEquals(0, str.lastIndexOf(1, -10))
+        assertEquals(3, ByteString(0, 1, 1, 1, 0).lastIndexOf(1))
+
+        assertEquals(-1, ByteString().lastIndexOf(0))
+        assertEquals(-1, ByteString().lastIndexOf(0, 100500))
+        assertEquals(-1, str.lastIndexOf(1, 1005000))
+    }
+
+    @Test
+    fun lastIndexOfByteArray() {
+        val str = ByteString(1, 2, 3, 4, 5)
+
+        assertEquals(0, str.lastIndexOf(byteArrayOf(1, 2, 3, 4, 5)))
+        assertEquals(0, str.lastIndexOf(byteArrayOf(1, 2, 3)))
+        assertEquals(-1, str.lastIndexOf(byteArrayOf(0, 1, 2)))
+        assertEquals(2, str.lastIndexOf(byteArrayOf(3, 4, 5)))
+        assertEquals(-1, str.lastIndexOf(byteArrayOf(1, 2, 3), 1))
+        assertEquals(1, str.lastIndexOf(byteArrayOf(2, 3, 4), 1))
+        assertEquals(str.size, str.lastIndexOf(byteArrayOf()))
+        assertEquals(str.size, str.lastIndexOf(byteArrayOf()))
+        assertEquals(2, str.lastIndexOf(byteArrayOf(3, 4), -1000))
+        assertEquals(0, str.lastIndexOf(byteArrayOf(1), -1))
+        assertEquals(4, ByteString(1, 1, 1, 1, 1).lastIndexOf(byteArrayOf(1)))
+        assertEquals(3, ByteString(0, 1, 0, 1, 0).lastIndexOf(byteArrayOf(1, 0)))
+        assertEquals(0, ByteString().lastIndexOf(byteArrayOf()))
+        assertEquals(0, ByteString().lastIndexOf(byteArrayOf(), -100500))
+        assertEquals(0, ByteString().lastIndexOf(byteArrayOf(), 100500))
+        assertEquals(-1, str.lastIndexOf(byteArrayOf(1, 2, 3), 100500))
+        assertEquals(-1, ByteString().lastIndexOf(byteArrayOf(1, 2, 3)))
+        assertEquals(-1, str.lastIndexOf(byteArrayOf(2, 3, 5)))
+    }
+
+    @Test
+    fun lastIndexOfByteString() {
+        val str = ByteString(1, 2, 3, 4, 5)
+
+        assertEquals(0, str.lastIndexOf(ByteString(1, 2, 3, 4, 5)))
+        assertEquals(0, str.lastIndexOf(ByteString(1, 2, 3)))
+        assertEquals(-1, str.lastIndexOf(ByteString(0, 1, 2)))
+        assertEquals(2, str.lastIndexOf(ByteString(3, 4, 5)))
+        assertEquals(-1, str.lastIndexOf(ByteString(1, 2, 3), 1))
+        assertEquals(1, str.lastIndexOf(ByteString(2, 3, 4), 1))
+        assertEquals(str.size, str.lastIndexOf(ByteString()))
+        assertEquals(str.size, str.lastIndexOf(ByteString()))
+        assertEquals(2, str.lastIndexOf(ByteString(3, 4), -1000))
+        assertEquals(0, str.lastIndexOf(ByteString(1), -1))
+        assertEquals(4, ByteString(1, 1, 1, 1, 1).lastIndexOf(ByteString(1)))
+        assertEquals(3, ByteString(0, 1, 0, 1, 0).lastIndexOf(ByteString(1, 0)))
+        assertEquals(0, ByteString().lastIndexOf(ByteString()))
+        assertEquals(0, ByteString().lastIndexOf(ByteString(), -100500))
+        assertEquals(0, ByteString().lastIndexOf(ByteString(), 100500))
+        assertEquals(-1, str.lastIndexOf(ByteString(1, 2, 3), 100500))
+        assertEquals(-1, ByteString().lastIndexOf(ByteString(1, 2, 3)))
+        assertEquals(-1, str.lastIndexOf(ByteString(2, 3, 5)))
+    }
+
+    @Test
+    fun startsWithByteArray() {
+        val str = ByteString(1, 2, 3, 4, 5)
+
+        assertTrue(str.startsWith(byteArrayOf(1, 2, 3, 4, 5)))
+        assertTrue(str.startsWith(byteArrayOf(1, 2, 3)))
+
+        assertTrue(str.startsWith(byteArrayOf()))
+
+        assertFalse(str.startsWith(byteArrayOf(0, 1, 2, 3)))
+        assertFalse(str.startsWith(byteArrayOf(2, 3, 4)))
+        assertFalse(str.startsWith(byteArrayOf(1, 2, 3, 4, 5, 6)))
+
+        assertTrue(ByteString().startsWith(byteArrayOf()))
+    }
+
+    @Test
+    fun startWithByteString() {
+        val str = ByteString(1, 2, 3, 4, 5)
+
+        assertTrue(str.startsWith(ByteString(1, 2, 3, 4, 5)))
+        assertTrue(str.startsWith(ByteString(1, 2, 3)))
+
+        assertTrue(str.startsWith(ByteString()))
+
+        assertFalse(str.startsWith(ByteString(0, 1, 2, 3)))
+        assertFalse(str.startsWith(ByteString(2, 3, 4)))
+        assertFalse(str.startsWith(ByteString(1, 2, 3, 4, 5, 6)))
+
+        assertTrue(ByteString().startsWith(ByteString()))
+    }
+
+    @Test
+    fun endsWithByteArray() {
+        val str = ByteString(1, 2, 3, 4, 5)
+
+        assertTrue(str.endsWith(byteArrayOf(1, 2, 3, 4, 5)))
+        assertTrue(str.endsWith(byteArrayOf(3, 4, 5)))
+
+        assertTrue(str.endsWith(byteArrayOf()))
+
+        assertFalse(str.endsWith(byteArrayOf(3, 4, 5, 6)))
+        assertFalse(str.endsWith(byteArrayOf(0, 1, 2, 3, 4, 5)))
+        assertFalse(str.endsWith(byteArrayOf(2, 3, 4)))
+
+        assertTrue(ByteString().endsWith(byteArrayOf()))
+    }
+
+    @Test
+    fun endsWithByteString() {
+        val str = ByteString(1, 2, 3, 4, 5)
+
+        assertTrue(str.endsWith(ByteString(1, 2, 3, 4, 5)))
+        assertTrue(str.endsWith(ByteString(3, 4, 5)))
+
+        assertTrue(str.endsWith(ByteString()))
+
+        assertFalse(str.endsWith(ByteString(3, 4, 5, 6)))
+        assertFalse(str.endsWith(ByteString(0, 1, 2, 3, 4, 5)))
+        assertFalse(str.endsWith(ByteString(2, 3, 4)))
+
+        assertTrue(ByteString().endsWith(ByteString()))
+    }
+
+    @Test
+    fun testToString() {
+        assertEquals("ByteString(size=0)", ByteString().toString())
+        assertEquals("ByteString(size=1 hex=00)", ByteString(0).toString())
+        assertEquals(
+            "ByteString(size=16 hex=000102030405060708090A0B0C0D0E0F)",
+            ByteString(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15).toString()
+        )
+        assertEquals(
+            "ByteString(size=64 hex=0000000000000000000000000000000000000000000000000000000000000000" +
+                    "0000000000000000000000000000000000000000000000000000000000000000)",
+            ByteString(ByteArray(64)).toString()
+        )
+    }
+
+    private val bronzeHorseman = "На берегу пустынных волн"
+
+    @Test
+    fun utf8() {
+        val byteString = bronzeHorseman.encodeToByteString()
+        assertEquals(byteString.toByteArray().toList(), bronzeHorseman.encodeToByteArray().toList())
+        assertEquals(byteString, ByteString(*bronzeHorseman.encodeToByteArray()))
+        assertEquals(byteString.decodeToString(), bronzeHorseman)
+    }
+
+    @Test
+    fun contentEquals() {
+        assertTrue(ByteString().contentEquals(byteArrayOf()))
+        assertFalse(ByteString(1, 2, 3).contentEquals(byteArrayOf()))
+        assertFalse(ByteString().contentEquals(byteArrayOf(1, 2, 3)))
+
+        assertTrue(ByteString(1, 2, 3, 4, 5).contentEquals(byteArrayOf(1, 2, 3, 4, 5)))
+        assertFalse(ByteString(1, 2, 3, 4, 5).contentEquals(byteArrayOf(1, 2, 3, 4, 4)))
+        assertFalse(ByteString(1, 2, 3, 4, 5).contentEquals(byteArrayOf(1, 2, 3, 4, 5, 6)))
+        assertFalse(ByteString(1, 2, 3, 4, 5, 6).contentEquals(byteArrayOf(1, 2, 3, 4, 5)))
+    }
+}
diff --git a/bytestring/jvm/src/ByteStringJvmExt.kt b/bytestring/jvm/src/ByteStringJvmExt.kt
new file mode 100644
index 00000000..555e65f8
--- /dev/null
+++ b/bytestring/jvm/src/ByteStringJvmExt.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+package kotlinx.io.bytestring
+
+import java.nio.charset.Charset
+
+/**
+ * Decodes the content of a byte string to a string using given [charset].
+ *
+ * @param charset the charset to decode data into a string.
+ */
+public fun ByteString.decodeToString(charset: Charset): String = getBackingArrayReference().toString(charset)
+
+/**
+ * Encodes a string into a byte string using [charset].
+ *
+ * @param charset the encoding.
+ */
+public fun String.encodeToByteString(charset: Charset): ByteString = ByteString.wrap(toByteArray(charset))
diff --git a/bytestring/jvm/test/ByteStringJvmTest.kt b/bytestring/jvm/test/ByteStringJvmTest.kt
new file mode 100644
index 00000000..37f9b86a
--- /dev/null
+++ b/bytestring/jvm/test/ByteStringJvmTest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+package kotlinx.io.bytestring
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+/**
+ * Set of tests covering JVM-specific [ByteString] extensions.
+ */
+class ByteStringJvmTest {
+    @Test
+    fun createFromString() {
+        val str = "hello"
+
+        assertEquals(ByteString(byteArrayOf(0x68, 0x65, 0x6c, 0x6c, 0x6f)), str.encodeToByteString(Charsets.UTF_8))
+        assertEquals(
+            ByteString(
+                byteArrayOf(
+                    0, 0, 0, 0x68, 0, 0, 0, 0x65, 0, 0, 0, 0x6c,
+                    0, 0, 0, 0x6c, 0, 0, 0, 0x6f
+                )
+            ), str.encodeToByteString(Charsets.UTF_32)
+        )
+    }
+
+    @Test
+    fun decodeToString() {
+        assertEquals(
+            "Ϭ",
+            ByteString(0xfeU.toByte(), 0xffU.toByte(), 0x03, 0xecU.toByte()).decodeToString(Charsets.UTF_16)
+        )
+
+        assertEquals("123", ByteString("123".encodeToByteArray()).decodeToString(Charsets.UTF_8))
+    }
+}
diff --git a/core/common/src/ByteStringExt.kt b/core/common/src/ByteStringExt.kt
new file mode 100644
index 00000000..b5f0f603
--- /dev/null
+++ b/core/common/src/ByteStringExt.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
+ */
+
+package kotlinx.io
+
+import kotlinx.io.bytestring.ByteString
+import kotlinx.io.bytestring.isEmpty
+import kotlinx.io.bytestring.unsafe.UnsafeByteStringApi
+import kotlinx.io.bytestring.unsafe.UnsafeByteStringOperations
+import kotlin.math.min
+
+/**
+ * Writes subsequence of data from [byteString] starting at [startIndex] and ending at [endIndex] into a sink.
+ *
+ * @param byteString the byte string whose subsequence should be written to a sink.
+ * @param startIndex the first index (inclusive) to copy data from the [byteString].
+ * @param endIndex the last index (exclusive) to copy data from the [byteString]
+ *
+ * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [byteString] indices.
+ * @throws IllegalArgumentException when `startIndex > endIndex`.
+ * @throws IllegalStateException if the sink is closed.
+ */
+@OptIn(DelicateIoApi::class)
+public fun Sink.write(byteString: ByteString, startIndex: Int = 0, endIndex: Int = byteString.size) {
+    checkBounds(byteString.size, startIndex, endIndex)
+    if (endIndex == startIndex) {
+        return
+    }
+
+    writeToInternalBuffer { buffer ->
+        var offset = startIndex
+        val tail = buffer.head?.prev
+        if (tail != null) {
+            val bytesToWrite = min(tail.data.size - tail.limit, endIndex - offset)
+            byteString.copyInto(tail.data, tail.limit, offset, offset + bytesToWrite)
+            offset += bytesToWrite
+            tail.limit += bytesToWrite
+            buffer.size += bytesToWrite
+        }
+        while (offset < endIndex) {
+            val bytesToWrite = min(endIndex - offset, Segment.SIZE)
+            val seg = buffer.writableSegment(bytesToWrite)
+            byteString.copyInto(seg.data, seg.limit, offset, offset + bytesToWrite)
+            seg.limit += bytesToWrite
+            buffer.size += bytesToWrite
+            offset += bytesToWrite
+        }
+    }
+}
+
+/**
+ * Consumes all bytes from this source and wraps it into a byte string.
+ *
+ * @throws IllegalStateException if the source is closed.
+ */
+@OptIn(UnsafeByteStringApi::class)
+public fun Source.readByteString(): ByteString {
+    return UnsafeByteStringOperations.wrapUnsafe(readByteArray())
+}
+
+/**
+ * Consumes exactly [byteCount] bytes from this source and wraps it into a byte string.
+ *
+ * @param byteCount the number of bytes to read from the source.
+ *
+ * @throws EOFException when the source is exhausted before reading [byteCount] bytes from it.
+ * @throws IllegalArgumentException when [byteCount] is negative.
+ * @throws IllegalStateException if the source is closed.
+ */
+@OptIn(UnsafeByteStringApi::class)
+public fun Source.readByteString(byteCount: Int): ByteString {
+    return UnsafeByteStringOperations.wrapUnsafe(readByteArray(byteCount))
+}
+
+/**
+ * Returns the index of the first match for [byteString] in the source at or after [startIndex]. This
+ * expands the source's buffer as necessary until [byteString] 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.
+ *
+ * @param byteString the sequence of bytes to find within the source.
+ * @param startIndex the index into the source to start searching from.
+ *
+ * @throws IllegalArgumentException if [startIndex] is negative.
+ * @throws IllegalStateException if the source is closed.
+ */
+@OptIn(InternalIoApi::class, UnsafeByteStringApi::class)
+public fun Source.indexOf(byteString: ByteString, startIndex: Long = 0): Long {
+    require(startIndex >= 0) { "startIndex: $startIndex" }
+
+    if (byteString.isEmpty()) {
+        return 0
+    }
+
+    var offset = startIndex
+    val peek = peek()
+    if (!request(startIndex)) {
+        return -1L
+    }
+    peek.skip(offset)
+    var resultingIndex = -1L
+    UnsafeByteStringOperations.withByteArrayUnsafe(byteString) { data ->
+        while (!peek.exhausted()) {
+            val index = peek.indexOf(data[0])
+            if (index == -1L) {
+                return@withByteArrayUnsafe
+            }
+            offset += index
+            peek.skip(index)
+            if (!peek.request(byteString.size.toLong())) {
+                return@withByteArrayUnsafe
+            }
+
+            var matches = true
+            for (idx in data.indices) {
+                if (data[idx] != peek.buffer[idx.toLong()]) {
+                    matches = false
+                    offset++
+                    peek.skip(1)
+                    break
+                }
+            }
+            if (matches) {
+                resultingIndex = offset
+                return@withByteArrayUnsafe
+            }
+        }
+    }
+    return resultingIndex
+}
diff --git a/core/common/test/AbstractSinkTest.kt b/core/common/test/AbstractSinkTest.kt
index 38882154..22e048d6 100644
--- a/core/common/test/AbstractSinkTest.kt
+++ b/core/common/test/AbstractSinkTest.kt
@@ -21,6 +21,8 @@
 
 package kotlinx.io
 
+import kotlinx.io.bytestring.ByteString
+import kotlinx.io.bytestring.encodeToByteString
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -65,7 +67,7 @@ abstract class AbstractSinkTest internal constructor(
 
     @Test
     fun writeNothing() {
-        sink.writeUtf8("")
+        sink.writeString("")
         sink.flush()
         assertEquals(0, data.size)
     }
@@ -87,12 +89,12 @@ abstract class AbstractSinkTest internal constructor(
 
     @Test
     fun writeLastByteInSegment() {
-        sink.writeUtf8("a".repeat(Segment.SIZE - 1))
+        sink.writeString("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("a".repeat(Segment.SIZE - 1), data.readString(Segment.SIZE - 1L))
         assertEquals("Buffer(size=2 hex=2021)", data.toString())
     }
 
@@ -136,23 +138,23 @@ abstract class AbstractSinkTest internal constructor(
 
     @Test
     fun writeLastIntegerInSegment() {
-        sink.writeUtf8("a".repeat(Segment.SIZE - 4))
+        sink.writeString("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("a".repeat(Segment.SIZE - 4), data.readString(Segment.SIZE - 4L))
         assertEquals("Buffer(size=8 hex=abcdef0187654321)", data.toString())
     }
 
     @Test
     fun writeIntegerDoesNotQuiteFitInSegment() {
-        sink.writeUtf8("a".repeat(Segment.SIZE - 3))
+        sink.writeString("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("a".repeat(Segment.SIZE - 3), data.readString(Segment.SIZE - 3L))
         assertEquals("Buffer(size=8 hex=abcdef0187654321)", data.toString())
     }
 
@@ -190,12 +192,12 @@ abstract class AbstractSinkTest internal constructor(
     @Test
     fun writeAll() {
         val source = Buffer()
-        source.writeUtf8("abcdef")
+        source.writeString("abcdef")
 
         assertEquals(6, sink.transferFrom(source))
         assertEquals(0, source.size)
         sink.flush()
-        assertEquals("abcdef", data.readUtf8())
+        assertEquals("abcdef", data.readString())
     }
 
     @Test
@@ -208,32 +210,32 @@ abstract class AbstractSinkTest internal constructor(
     @Test
     fun writeSource() {
         val source = Buffer()
-        source.writeUtf8("abcdef")
+        source.writeString("abcdef")
 
         // Force resolution of the Source method overload.
         sink.write(source as RawSource, 4)
         sink.flush()
-        assertEquals("abcd", data.readUtf8())
-        assertEquals("ef", source.readUtf8())
+        assertEquals("abcd", data.readString())
+        assertEquals("ef", source.readString())
     }
 
     @Test
     fun writeSourceReadsFully() {
         val source = object : RawSource by Buffer() {
             override fun readAtMostTo(sink: Buffer, byteCount: Long): Long {
-                sink.writeUtf8("abcd")
+                sink.writeString("abcd")
                 return 4
             }
         }
 
         sink.write(source, 8)
         sink.flush()
-        assertEquals("abcdabcd", data.readUtf8())
+        assertEquals("abcdabcd", data.readString())
     }
 
     @Test
     fun writeSourcePropagatesEof() {
-        val source: RawSource = Buffer().also { it.writeUtf8("abcd") }
+        val source: RawSource = Buffer().also { it.writeString("abcd") }
 
         assertFailsWith<EOFException> {
             sink.write(source, 8)
@@ -241,20 +243,20 @@ abstract class AbstractSinkTest internal constructor(
 
         // Ensure that whatever was available was correctly written.
         sink.flush()
-        assertEquals("abcd", data.readUtf8())
+        assertEquals("abcd", data.readString())
     }
 
     @Test
     fun writeBufferThrowsIAE() {
         val source = Buffer()
-        source.writeUtf8("abcd")
+        source.writeString("abcd")
 
         assertFailsWith<IllegalArgumentException> {
             sink.write(source, 8)
         }
 
         sink.flush()
-        assertEquals("", data.readUtf8())
+        assertEquals("", data.readString())
     }
 
     @Test
@@ -346,11 +348,11 @@ abstract class AbstractSinkTest internal constructor(
     private fun assertLongDecimalString(string: String, value: Long) {
         with(sink) {
             writeDecimalLong(value)
-            writeUtf8("zzz")
+            writeString("zzz")
             flush()
         }
         val expected = "${string}zzz"
-        val actual = data.readUtf8()
+        val actual = data.readString()
         assertEquals(expected, actual, "$value expected $expected but was $actual")
     }
 
@@ -369,33 +371,33 @@ abstract class AbstractSinkTest internal constructor(
     private fun assertLongHexString(value: Long) {
         with(sink) {
             writeHexadecimalUnsignedLong(value)
-            writeUtf8("zzz")
+            writeString("zzz")
             flush()
         }
         val expected = "${value.toHexString()}zzz"
-        val actual = data.readUtf8()
+        val actual = data.readString()
         assertEquals(expected, actual, "$value expected $expected but was $actual")
     }
 
     @Test
     fun writeUtf8FromIndex() {
-        sink.writeUtf8("12345", 3)
+        sink.writeString("12345", 3)
         sink.emit()
-        assertEquals("45", data.readUtf8())
+        assertEquals("45", data.readString())
     }
 
     @Test
     fun writeUtf8FromRange() {
-        sink.writeUtf8("0123456789", 4, 7)
+        sink.writeString("0123456789", 4, 7)
         sink.emit()
-        assertEquals("456", data.readUtf8())
+        assertEquals("456", data.readString())
     }
 
     @Test
     fun writeUtf8WithInvalidIndexes() {
-        assertFailsWith<IndexOutOfBoundsException> { sink.writeUtf8("hello", startIndex = -1) }
-        assertFailsWith<IndexOutOfBoundsException> { sink.writeUtf8("hello", startIndex = 0, endIndex = 6) }
-        assertFailsWith<IllegalArgumentException> { sink.writeUtf8("hello", startIndex = 6) }
+        assertFailsWith<IndexOutOfBoundsException> { sink.writeString("hello", startIndex = -1) }
+        assertFailsWith<IndexOutOfBoundsException> { sink.writeString("hello", startIndex = 0, endIndex = 6) }
+        assertFailsWith<IllegalArgumentException> { sink.writeString("hello", startIndex = 6) }
     }
 
     @Test
@@ -446,4 +448,18 @@ abstract class AbstractSinkTest internal constructor(
         sink.flush()
         assertEquals("Buffer(size=8 hex=efcdab9078563412)", data.toString())
     }
+
+    @Test
+    fun writeByteString() {
+        sink.write("təˈranəˌsôr".encodeToByteString())
+        sink.flush()
+        assertEquals(ByteString("74c999cb8872616ec999cb8c73c3b472".decodeHex()), data.readByteString())
+    }
+
+    @Test
+    fun writeByteStringOffset() {
+        sink.write("təˈranəˌsôr".encodeToByteString(), 5, 10)
+        sink.flush()
+        assertEquals(ByteString("72616ec999".decodeHex()), data.readByteString())
+    }
 }
diff --git a/core/common/test/AbstractSourceTest.kt b/core/common/test/AbstractSourceTest.kt
index e89f418b..c27f665f 100644
--- a/core/common/test/AbstractSourceTest.kt
+++ b/core/common/test/AbstractSourceTest.kt
@@ -21,6 +21,9 @@
 
 package kotlinx.io
 
+import kotlinx.io.bytestring.ByteString
+import kotlinx.io.bytestring.decodeToString
+import kotlinx.io.bytestring.encodeToByteString
 import kotlin.test.*
 
 private const val SEGMENT_SIZE = Segment.SIZE
@@ -85,7 +88,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readShortSplitAcrossMultipleSegments() {
-        sink.writeUtf8("a".repeat(Segment.SIZE - 1))
+        sink.writeString("a".repeat(Segment.SIZE - 1))
         sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte()))
         sink.emit()
         source.skip((Segment.SIZE - 1).toLong())
@@ -157,7 +160,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readIntSplitAcrossMultipleSegments() {
-        sink.writeUtf8("a".repeat(Segment.SIZE - 3))
+        sink.writeString("a".repeat(Segment.SIZE - 3))
         sink.write(byteArrayOf(0xab.toByte(), 0xcd.toByte(), 0xef.toByte(), 0x01.toByte()))
         sink.emit()
         source.skip((Segment.SIZE - 3).toLong())
@@ -245,7 +248,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readLongSplitAcrossMultipleSegments() {
-        sink.writeUtf8("a".repeat(Segment.SIZE - 7))
+        sink.writeString("a".repeat(Segment.SIZE - 7))
         sink.write(
             byteArrayOf(
                 0xab.toByte(),
@@ -289,13 +292,13 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @OptIn(InternalIoApi::class)
     @Test
     fun transferTo() {
-        source.buffer.writeUtf8("abc")
-        sink.writeUtf8("def")
+        source.buffer.writeString("abc")
+        sink.writeString("def")
         sink.emit()
 
         val sink = Buffer()
         assertEquals(6, source.transferTo(sink))
-        assertEquals("abcdef", sink.readUtf8())
+        assertEquals("abcdef", sink.readString())
         assertTrue(source.exhausted())
     }
 
@@ -310,7 +313,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @Test
     fun readExhaustedSource() {
         val sink = Buffer()
-        sink.writeUtf8("a".repeat(10))
+        sink.writeString("a".repeat(10))
         assertEquals(-1, source.readAtMostTo(sink, 10))
         assertEquals(10, sink.size)
         assertTrue(source.exhausted())
@@ -319,7 +322,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @Test
     fun readZeroBytesFromSource() {
         val sink = Buffer()
-        sink.writeUtf8("a".repeat(10))
+        sink.writeString("a".repeat(10))
 
         // Either 0 or -1 is reasonable here. For consistency with Android's
         // ByteArrayInputStream we return 0.
@@ -391,17 +394,17 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readToSink() {
-        sink.writeUtf8("a".repeat(10000))
+        sink.writeString("a".repeat(10000))
         sink.emit()
         val sink = Buffer()
         source.readTo(sink, 9999)
-        assertEquals("a".repeat(9999), sink.readUtf8())
-        assertEquals("a", source.readUtf8())
+        assertEquals("a".repeat(9999), sink.readString())
+        assertEquals("a", source.readString())
     }
 
     @Test
     fun readToSinkTooShortThrows() {
-        sink.writeUtf8("Hi")
+        sink.writeString("Hi")
         sink.emit()
         val sink = Buffer()
         assertFailsWith<EOFException> {
@@ -409,7 +412,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
         }
 
         // Verify we read all that we could from the source.
-        assertEquals("Hi", sink.readUtf8())
+        assertEquals("Hi", sink.readString())
         assertTrue(source.exhausted())
     }
 
@@ -423,19 +426,19 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readToSinkZeroBytes() {
-        sink.writeUtf8("test")
+        sink.writeString("test")
         sink.flush()
         val sink = Buffer()
         source.readTo(sink, 0)
         assertEquals(0, sink.size)
-        assertEquals("test", source.readUtf8())
+        assertEquals("test", source.readString())
     }
 
     @Test
     fun readToByteArray() {
         val data = Buffer()
-        data.writeUtf8("Hello")
-        data.writeUtf8("e".repeat(Segment.SIZE))
+        data.writeString("Hello")
+        data.writeString("e".repeat(Segment.SIZE))
 
         val expected = data.copy().readByteArray()
         sink.write(data, data.size)
@@ -453,13 +456,13 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
         val sink = ByteArray(8)
 
-        buffer.writeUtf8("hello")
+        buffer.writeString("hello")
         source.readTo(sink, 0, 3)
         assertContentEquals(byteArrayOf('h'.code.toByte(), 'e'.code.toByte(), 'l'.code.toByte(), 0, 0, 0, 0, 0), sink)
-        assertEquals("lo", source.readUtf8())
+        assertEquals("lo", source.readString())
 
         sink.fill(0)
-        buffer.writeUtf8("hello")
+        buffer.writeString("hello")
         source.readTo(sink, 3)
         assertContentEquals(
             byteArrayOf(
@@ -470,10 +473,10 @@ abstract class AbstractBufferedSourceTest internal constructor(
         assertTrue(source.exhausted())
 
         sink.fill(0)
-        buffer.writeUtf8("hello")
+        buffer.writeString("hello")
         source.readTo(sink, 3, 4)
         assertContentEquals(byteArrayOf(0, 0, 0, 'h'.code.toByte(), 0, 0, 0, 0), sink)
-        assertEquals("ello", source.readUtf8())
+        assertEquals("ello", source.readString())
     }
 
     @Test
@@ -489,7 +492,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readToByteArrayTooShortThrows() {
-        sink.writeUtf8("Hello")
+        sink.writeString("Hello")
         sink.emit()
 
         val array = ByteArray(6)
@@ -513,7 +516,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readAtMostToByteArray() {
-        sink.writeUtf8("abcd")
+        sink.writeString("abcd")
         sink.emit()
 
         val sink = ByteArray(3)
@@ -531,7 +534,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readAtMostToByteArrayNotEnough() {
-        sink.writeUtf8("abcd")
+        sink.writeString("abcd")
         sink.emit()
 
         val sink = ByteArray(5)
@@ -550,7 +553,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readAtMostToByteArrayOffsetAndCount() {
-        sink.writeUtf8("abcd")
+        sink.writeString("abcd")
         sink.emit()
 
         val sink = ByteArray(7)
@@ -570,7 +573,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readAtMostToByteArrayFromOffset() {
-        sink.writeUtf8("abcd")
+        sink.writeString("abcd")
         sink.emit()
 
         val sink = ByteArray(7)
@@ -610,28 +613,28 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @Test
     fun readByteArray() {
         val string = "abcd" + "e".repeat(Segment.SIZE)
-        sink.writeUtf8(string)
+        sink.writeString(string)
         sink.emit()
         assertArrayEquals(string.asUtf8ToByteArray(), source.readByteArray())
     }
 
     @Test
     fun readByteArrayPartial() {
-        sink.writeUtf8("abcd")
+        sink.writeString("abcd")
         sink.emit()
         assertEquals("[97, 98, 99]", source.readByteArray(3).contentToString())
-        assertEquals("d", source.readUtf8(1))
+        assertEquals("d", source.readString(1))
     }
 
     @Test
     fun readByteArrayTooShortThrows() {
-        sink.writeUtf8("abc")
+        sink.writeString("abc")
         sink.emit()
         assertFailsWith<EOFException> {
             source.readByteArray(4)
         }
 
-        assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data.
+        assertEquals("abc", source.readString()) // The read shouldn't consume any data.
     }
 
     @Test
@@ -641,49 +644,49 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readUtf8SpansSegments() {
-        sink.writeUtf8("a".repeat(Segment.SIZE * 2))
+        sink.writeString("a".repeat(Segment.SIZE * 2))
         sink.emit()
         source.skip((Segment.SIZE - 1).toLong())
-        assertEquals("aa", source.readUtf8(2))
+        assertEquals("aa", source.readString(2))
     }
 
     @Test
     fun readUtf8Segment() {
-        sink.writeUtf8("a".repeat(Segment.SIZE))
+        sink.writeString("a".repeat(Segment.SIZE))
         sink.emit()
-        assertEquals("a".repeat(Segment.SIZE), source.readUtf8(Segment.SIZE.toLong()))
+        assertEquals("a".repeat(Segment.SIZE), source.readString(Segment.SIZE.toLong()))
     }
 
     @Test
     fun readUtf8PartialBuffer() {
-        sink.writeUtf8("a".repeat(Segment.SIZE + 20))
+        sink.writeString("a".repeat(Segment.SIZE + 20))
         sink.emit()
-        assertEquals("a".repeat(Segment.SIZE + 10), source.readUtf8((Segment.SIZE + 10).toLong()))
+        assertEquals("a".repeat(Segment.SIZE + 10), source.readString((Segment.SIZE + 10).toLong()))
     }
 
     @Test
     fun readUtf8EntireBuffer() {
-        sink.writeUtf8("a".repeat(Segment.SIZE * 2))
+        sink.writeString("a".repeat(Segment.SIZE * 2))
         sink.emit()
-        assertEquals("a".repeat(Segment.SIZE * 2), source.readUtf8())
+        assertEquals("a".repeat(Segment.SIZE * 2), source.readString())
     }
 
     @Test
     fun readUtf8TooShortThrows() {
-        sink.writeUtf8("abc")
+        sink.writeString("abc")
         sink.emit()
         assertFailsWith<EOFException> {
-            source.readUtf8(4L)
+            source.readString(4L)
         }
 
-        assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data.
+        assertEquals("abc", source.readString()) // The read shouldn't consume any data.
     }
 
     @Test
     fun skip() {
-        sink.writeUtf8("a")
-        sink.writeUtf8("b".repeat(Segment.SIZE))
-        sink.writeUtf8("c")
+        sink.writeString("a")
+        sink.writeString("b".repeat(Segment.SIZE))
+        sink.writeString("c")
         sink.emit()
         source.skip(1)
         assertEquals('b'.code.toLong(), (source.readByte() and 0xff).toLong())
@@ -695,7 +698,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun skipInsufficientData() {
-        sink.writeUtf8("a")
+        sink.writeString("a")
         sink.emit()
         assertFailsWith<EOFException> {
             source.skip(2)
@@ -713,13 +716,13 @@ abstract class AbstractBufferedSourceTest internal constructor(
         assertEquals(-1, source.indexOf('a'.code.toByte()))
 
         // The segment has one value.
-        sink.writeUtf8("a") // a
+        sink.writeString("a") // a
         sink.emit()
         assertEquals(0, source.indexOf('a'.code.toByte()))
         assertEquals(-1, source.indexOf('b'.code.toByte()))
 
         // The segment has lots of data.
-        sink.writeUtf8("b".repeat(Segment.SIZE - 2)) // ab...b
+        sink.writeString("b".repeat(Segment.SIZE - 2)) // ab...b
         sink.emit()
         assertEquals(0, source.indexOf('a'.code.toByte()))
         assertEquals(1, source.indexOf('b'.code.toByte()))
@@ -732,7 +735,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
         assertEquals(-1, source.indexOf('c'.code.toByte()))
 
         // The segment is full.
-        sink.writeUtf8("c") // b...bc
+        sink.writeString("c") // b...bc
         sink.emit()
         assertEquals(-1, source.indexOf('a'.code.toByte()))
         assertEquals(0, source.indexOf('b'.code.toByte()))
@@ -745,7 +748,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
         assertEquals((Segment.SIZE - 5).toLong(), source.indexOf('c'.code.toByte()))
 
         // Two segments.
-        sink.writeUtf8("d") // b...bcd, d is in the 2nd segment.
+        sink.writeString("d") // b...bcd, d is in the 2nd segment.
         sink.emit()
         assertEquals((Segment.SIZE - 4).toLong(), source.indexOf('d'.code.toByte()))
         assertEquals(-1, source.indexOf('e'.code.toByte()))
@@ -754,9 +757,9 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @Test
     fun indexOfByteWithStartOffset() {
         with(sink) {
-            writeUtf8("a")
-            writeUtf8("b".repeat(Segment.SIZE))
-            writeUtf8("c")
+            writeString("a")
+            writeString("b".repeat(Segment.SIZE))
+            writeString("c")
             emit()
         }
         assertEquals(-1, source.indexOf('a'.code.toByte(), 1))
@@ -814,14 +817,14 @@ abstract class AbstractBufferedSourceTest internal constructor(
             assertEquals(-1, source.indexOf(c, p.toLong(), p.toLong()))
 
             // Reset.
-            source.readUtf8()
+            source.readString()
             bytes[p] = a
         }
     }
 
     @Test
     fun indexOfByteInvalidBoundsThrows() {
-        sink.writeUtf8("abc")
+        sink.writeString("abc")
         sink.emit()
         assertFailsWith<IllegalArgumentException>("Expected failure: fromIndex < 0") {
             source.indexOf('a'.code.toByte(), -1)
@@ -833,7 +836,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun indexOfByteWithFromIndex() {
-        sink.writeUtf8("aaa")
+        sink.writeString("aaa")
         sink.emit()
         assertEquals(0, source.indexOf('a'.code.toByte()))
         assertEquals(0, source.indexOf('a'.code.toByte(), 0))
@@ -844,9 +847,9 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @Test
     fun request() {
         with(sink) {
-            writeUtf8("a")
-            writeUtf8("b".repeat(Segment.SIZE))
-            writeUtf8("c")
+            writeString("a")
+            writeString("b".repeat(Segment.SIZE))
+            writeString("c")
             emit()
         }
         assertTrue(source.request((Segment.SIZE + 2).toLong()))
@@ -866,9 +869,9 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @Test
     fun require() {
         with(sink) {
-            writeUtf8("a")
-            writeUtf8("b".repeat(Segment.SIZE))
-            writeUtf8("c")
+            writeString("a")
+            writeString("b".repeat(Segment.SIZE))
+            writeString("c")
             emit()
         }
         source.require((Segment.SIZE + 2).toLong())
@@ -911,7 +914,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
     }
 
     private fun assertLongHexString(s: String, expected: Long) {
-        sink.writeUtf8(s)
+        sink.writeString(s)
         sink.emit()
         val actual = source.readHexadecimalUnsignedLong()
         assertEquals(expected, actual, "$s --> $expected")
@@ -920,8 +923,8 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @Test
     fun longHexStringAcrossSegment() {
         with(sink) {
-            writeUtf8("a".repeat(Segment.SIZE - 8))
-            writeUtf8("FFFFFFFFFFFFFFFF")
+            writeString("a".repeat(Segment.SIZE - 8))
+            writeString("FFFFFFFFFFFFFFFF")
             emit()
         }
         source.skip((Segment.SIZE - 8).toLong())
@@ -930,17 +933,17 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun longHexTerminatedByNonDigit() {
-        sink.writeUtf8("abcd,")
+        sink.writeString("abcd,")
         sink.emit()
         assertEquals(0xabcdL, source.readHexadecimalUnsignedLong())
     }
 
     @Test
     fun longHexAlphabet() {
-        sink.writeUtf8("7896543210abcdef")
+        sink.writeString("7896543210abcdef")
         sink.emit()
         assertEquals(0x7896543210abcdefL, source.readHexadecimalUnsignedLong())
-        sink.writeUtf8("ABCDEF")
+        sink.writeString("ABCDEF")
         sink.emit()
         assertEquals(0xabcdefL, source.readHexadecimalUnsignedLong())
     }
@@ -948,31 +951,31 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @Test
     fun longHexStringTooLongThrows() {
         val value = "fffffffffffffffff"
-        sink.writeUtf8(value)
+        sink.writeString(value)
         sink.emit()
 
         val e = assertFailsWith<NumberFormatException> {
             source.readHexadecimalUnsignedLong()
         }
         assertEquals("Number too large: fffffffffffffffff", e.message)
-        assertEquals(value, source.readUtf8())
+        assertEquals(value, source.readString())
     }
 
     @Test
     fun longHexStringTooShortThrows() {
-        sink.writeUtf8(" ")
+        sink.writeString(" ")
         sink.emit()
 
         val e = assertFailsWith<NumberFormatException> {
             source.readHexadecimalUnsignedLong()
         }
         assertEquals("Expected leading [0-9a-fA-F] character but was 0x20", e.message)
-        assertEquals(" ", source.readUtf8())
+        assertEquals(" ", source.readString())
     }
 
     @Test
     fun longHexEmptySourceThrows() {
-        sink.writeUtf8("")
+        sink.writeString("")
         sink.emit()
         assertFailsWith<EOFException> { source.readHexadecimalUnsignedLong() }
     }
@@ -990,81 +993,81 @@ abstract class AbstractBufferedSourceTest internal constructor(
     }
 
     private fun assertLongDecimalString(s: String, expected: Long) {
-        sink.writeUtf8(s)
-        sink.writeUtf8("zzz")
+        sink.writeString(s)
+        sink.writeString("zzz")
         sink.emit()
         val actual = source.readDecimalLong()
         assertEquals(expected, actual, "$s --> $expected")
-        assertEquals("zzz", source.readUtf8())
+        assertEquals("zzz", source.readString())
     }
 
     @Test
     fun longDecimalStringAcrossSegment() {
         with(sink) {
-            writeUtf8("a".repeat(Segment.SIZE - 8))
-            writeUtf8("1234567890123456")
-            writeUtf8("zzz")
+            writeString("a".repeat(Segment.SIZE - 8))
+            writeString("1234567890123456")
+            writeString("zzz")
             emit()
         }
         source.skip((Segment.SIZE - 8).toLong())
         assertEquals(1234567890123456L, source.readDecimalLong())
-        assertEquals("zzz", source.readUtf8())
+        assertEquals("zzz", source.readString())
     }
 
     @Test
     fun longDecimalStringTooLongThrows() {
         val value = "12345678901234567890"
-        sink.writeUtf8(value) // Too many digits.
+        sink.writeString(value) // Too many digits.
         sink.emit()
 
         val e = assertFailsWith<NumberFormatException> {
             source.readDecimalLong()
         }
         assertEquals("Number too large: 12345678901234567890", e.message)
-        assertEquals(value, source.readUtf8())
+        assertEquals(value, source.readString())
     }
 
     @Test
     fun longDecimalStringTooHighThrows() {
         val value = "9223372036854775808"
-        sink.writeUtf8(value) // Right size but cannot fit.
+        sink.writeString(value) // Right size but cannot fit.
         sink.emit()
 
         val e = assertFailsWith<NumberFormatException> {
             source.readDecimalLong()
         }
         assertEquals("Number too large: 9223372036854775808", e.message)
-        assertEquals(value, source.readUtf8())
+        assertEquals(value, source.readString())
     }
 
     @Test
     fun longDecimalStringTooLowThrows() {
         val value = "-9223372036854775809"
-        sink.writeUtf8(value) // Right size but cannot fit.
+        sink.writeString(value) // Right size but cannot fit.
         sink.emit()
 
         val e = assertFailsWith<NumberFormatException> {
             source.readDecimalLong()
         }
         assertEquals("Number too large: -9223372036854775809", e.message)
-        assertEquals(value, source.readUtf8())
+        assertEquals(value, source.readString())
     }
 
     @Test
     fun longDecimalStringTooShortThrows() {
-        sink.writeUtf8(" ")
+        sink.writeString(" ")
         sink.emit()
 
         val e = assertFailsWith<NumberFormatException> {
             source.readDecimalLong()
         }
         assertEquals("Expected a digit or '-' but was 0x20", e.message)
-        assertEquals(" ", source.readUtf8())
+        assertEquals(" ", source.readString())
     }
 
     @Test
     fun longDecimalEmptyThrows() {
-        sink.writeUtf8("")
+        sink.writeString("")
         sink.emit()
         assertFailsWith<EOFException> {
             source.readDecimalLong()
@@ -1073,22 +1076,22 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun longDecimalLoneDashThrows() {
-        sink.writeUtf8("-")
+        sink.writeString("-")
         sink.emit()
         assertFailsWith<EOFException> {
             source.readDecimalLong()
         }
-        assertEquals("-", source.readUtf8())
+        assertEquals("-", source.readString())
     }
 
     @Test
     fun longDecimalDashFollowedByNonDigitThrows() {
-        sink.writeUtf8("- ")
+        sink.writeString("- ")
         sink.emit()
         assertFailsWith<NumberFormatException> {
             source.readDecimalLong()
         }
-        assertEquals("- ", source.readUtf8())
+        assertEquals("- ", source.readString())
     }
 
     @Test
@@ -1151,39 +1154,39 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun peek() {
-        sink.writeUtf8("abcdefghi")
+        sink.writeString("abcdefghi")
         sink.emit()
 
-        assertEquals("abc", source.readUtf8(3))
+        assertEquals("abc", source.readString(3))
 
         val peek = source.peek()
-        assertEquals("def", peek.readUtf8(3))
-        assertEquals("ghi", peek.readUtf8(3))
+        assertEquals("def", peek.readString(3))
+        assertEquals("ghi", peek.readString(3))
         assertFalse(peek.request(1))
 
-        assertEquals("def", source.readUtf8(3))
+        assertEquals("def", source.readString(3))
     }
 
     @Test
     fun peekMultiple() {
-        sink.writeUtf8("abcdefghi")
+        sink.writeString("abcdefghi")
         sink.emit()
 
-        assertEquals("abc", source.readUtf8(3))
+        assertEquals("abc", source.readString(3))
 
         val peek1 = source.peek()
         val peek2 = source.peek()
 
-        assertEquals("def", peek1.readUtf8(3))
+        assertEquals("def", peek1.readString(3))
 
-        assertEquals("def", peek2.readUtf8(3))
-        assertEquals("ghi", peek2.readUtf8(3))
+        assertEquals("def", peek2.readString(3))
+        assertEquals("ghi", peek2.readString(3))
         assertFalse(peek2.request(1))
 
-        assertEquals("ghi", peek1.readUtf8(3))
+        assertEquals("ghi", peek1.readString(3))
         assertFalse(peek1.request(1))
 
-        assertEquals("def", source.readUtf8(3))
+        assertEquals("def", source.readString(3))
     }
 
     @Test
@@ -1192,40 +1195,40 @@ abstract class AbstractBufferedSourceTest internal constructor(
             // When run on CI this causes out-of-memory errors.
             return
         }
-        sink.writeUtf8("abcdef")
-        sink.writeUtf8("g".repeat(2 * Segment.SIZE))
-        sink.writeUtf8("hij")
+        sink.writeString("abcdef")
+        sink.writeString("g".repeat(2 * Segment.SIZE))
+        sink.writeString("hij")
         sink.emit()
 
-        assertEquals("abc", source.readUtf8(3))
+        assertEquals("abc", source.readString(3))
 
         val peek = source.peek()
-        assertEquals("def", peek.readUtf8(3))
+        assertEquals("def", peek.readString(3))
         peek.skip((2 * Segment.SIZE).toLong())
-        assertEquals("hij", peek.readUtf8(3))
+        assertEquals("hij", peek.readString(3))
         assertFalse(peek.request(1))
 
-        assertEquals("def", source.readUtf8(3))
+        assertEquals("def", source.readString(3))
         source.skip((2 * Segment.SIZE).toLong())
-        assertEquals("hij", source.readUtf8(3))
+        assertEquals("hij", source.readString(3))
     }
 
     @Test
     fun peekInvalid() {
-        sink.writeUtf8("abcdefghi")
+        sink.writeString("abcdefghi")
         sink.emit()
 
-        assertEquals("abc", source.readUtf8(3))
+        assertEquals("abc", source.readString(3))
 
         val peek = source.peek()
-        assertEquals("def", peek.readUtf8(3))
-        assertEquals("ghi", peek.readUtf8(3))
+        assertEquals("def", peek.readString(3))
+        assertEquals("ghi", peek.readString(3))
         assertFalse(peek.request(1))
 
-        assertEquals("def", source.readUtf8(3))
+        assertEquals("def", source.readString(3))
 
         val e = assertFailsWith<IllegalStateException> {
-            peek.readUtf8()
+            peek.readString()
         }
         assertEquals("Peek source is invalid because upstream source was used", e.message)
     }
@@ -1233,15 +1236,15 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @OptIn(InternalIoApi::class)
     @Test
     fun peekSegmentThenInvalid() {
-        sink.writeUtf8("abc")
-        sink.writeUtf8("d".repeat(2 * Segment.SIZE))
+        sink.writeString("abc")
+        sink.writeString("d".repeat(2 * Segment.SIZE))
         sink.emit()
 
-        assertEquals("abc", source.readUtf8(3))
+        assertEquals("abc", source.readString(3))
 
         // Peek a little data and skip the rest of the upstream source
         val peek = source.peek()
-        assertEquals("ddd", peek.readUtf8(3))
+        assertEquals("ddd", peek.readString(3))
         source.transferTo(discardingSink())
 
         // Skip the rest of the buffered data
@@ -1257,10 +1260,10 @@ abstract class AbstractBufferedSourceTest internal constructor(
     @Test
     fun peekDoesntReadTooMuch() {
         // 6 bytes in source's buffer plus 3 bytes upstream.
-        sink.writeUtf8("abcdef")
+        sink.writeString("abcdef")
         sink.emit()
         source.require(6L)
-        sink.writeUtf8("ghi")
+        sink.writeString("ghi")
         sink.emit()
 
         val peek = source.peek()
@@ -1271,7 +1274,7 @@ abstract class AbstractBufferedSourceTest internal constructor(
             assertEquals(6, source.buffer.size)
             assertEquals(6, peek.buffer.size)
         }
-        assertEquals("abc", peek.readUtf8(3L))
+        assertEquals("abc", peek.readString(3L))
 
         // Read 3 more bytes. This exhausts the buffered data.
         assertTrue(peek.request(3))
@@ -1279,19 +1282,19 @@ abstract class AbstractBufferedSourceTest internal constructor(
             assertEquals(6, source.buffer.size)
             assertEquals(3, peek.buffer.size)
         }
-        assertEquals("def", peek.readUtf8(3L))
+        assertEquals("def", peek.readString(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))
+        assertEquals("ghi", peek.readString(3L))
     }
 
     @OptIn(InternalIoApi::class)
     @Test
     fun factorySegmentSizes() {
-        sink.writeUtf8("abc")
+        sink.writeString("abc")
         sink.emit()
         source.require(3)
         if (factory.isOneByteAtATime) {
@@ -1303,73 +1306,73 @@ abstract class AbstractBufferedSourceTest internal constructor(
 
     @Test
     fun readUtf8Line() {
-        sink.writeUtf8("first line\nsecond line\n")
+        sink.writeString("first line\nsecond line\n")
         sink.flush()
-        assertEquals("first line", source.readUtf8Line())
-        assertEquals("second line\n", source.readUtf8())
-        assertEquals(null, source.readUtf8Line())
+        assertEquals("first line", source.readLine())
+        assertEquals("second line\n", source.readString())
+        assertEquals(null, source.readLine())
 
-        sink.writeUtf8("\nnext line\n")
+        sink.writeString("\nnext line\n")
         sink.flush()
-        assertEquals("", source.readUtf8Line())
-        assertEquals("next line", source.readUtf8Line())
+        assertEquals("", source.readLine())
+        assertEquals("next line", source.readLine())
 
-        sink.writeUtf8("There is no newline!")
+        sink.writeString("There is no newline!")
         sink.flush()
-        assertEquals("There is no newline!", source.readUtf8Line())
+        assertEquals("There is no newline!", source.readLine())
 
-        sink.writeUtf8("Wot do u call it?\r\nWindows")
+        sink.writeString("Wot do u call it?\r\nWindows")
         sink.flush()
-        assertEquals("Wot do u call it?", source.readUtf8Line())
+        assertEquals("Wot do u call it?", source.readLine())
         source.transferTo(discardingSink())
 
-        sink.writeUtf8("reo\rde\red\n")
+        sink.writeString("reo\rde\red\n")
         sink.flush()
-        assertEquals("reo\rde\red", source.readUtf8Line())
+        assertEquals("reo\rde\red", source.readLine())
     }
 
     @Test
     fun readUtf8LineStrict() {
-        sink.writeUtf8("first line\nsecond line\n")
+        sink.writeString("first line\nsecond line\n")
         sink.flush()
-        assertEquals("first line", source.readUtf8LineStrict())
-        assertEquals("second line\n", source.readUtf8())
-        assertFailsWith<EOFException> { source.readUtf8LineStrict() }
+        assertEquals("first line", source.readLineStrict())
+        assertEquals("second line\n", source.readString())
+        assertFailsWith<EOFException> { source.readLineStrict() }
 
-        sink.writeUtf8("\nnext line\n")
+        sink.writeString("\nnext line\n")
         sink.flush()
-        assertEquals("", source.readUtf8LineStrict())
-        assertEquals("next line", source.readUtf8LineStrict())
+        assertEquals("", source.readLineStrict())
+        assertEquals("next line", source.readLineStrict())
 
-        sink.writeUtf8("There is no newline!")
+        sink.writeString("There is no newline!")
         sink.flush()
-        assertFailsWith<EOFException> { source.readUtf8LineStrict() }
-        assertEquals("There is no newline!", source.readUtf8())
+        assertFailsWith<EOFException> { source.readLineStrict() }
+        assertEquals("There is no newline!", source.readString())
 
-        sink.writeUtf8("Wot do u call it?\r\nWindows")
+        sink.writeString("Wot do u call it?\r\nWindows")
         sink.flush()
-        assertEquals("Wot do u call it?", source.readUtf8LineStrict())
+        assertEquals("Wot do u call it?", source.readLineStrict())
         source.transferTo(discardingSink())
 
-        sink.writeUtf8("reo\rde\red\n")
+        sink.writeString("reo\rde\red\n")
         sink.flush()
-        assertEquals("reo\rde\red", source.readUtf8LineStrict())
+        assertEquals("reo\rde\red", source.readLineStrict())
 
-        sink.writeUtf8("line\n")
+        sink.writeString("line\n")
         sink.flush()
-        assertFailsWith<EOFException> { source.readUtf8LineStrict(3) }
-        assertEquals("line", source.readUtf8LineStrict(4))
+        assertFailsWith<EOFException> { source.readLineStrict(3) }
+        assertEquals("line", source.readLineStrict(4))
         assertTrue(source.exhausted())
 
-        sink.writeUtf8("line\r\n")
+        sink.writeString("line\r\n")
         sink.flush()
-        assertFailsWith<EOFException> { source.readUtf8LineStrict(3) }
-        assertEquals("line", source.readUtf8LineStrict(4))
+        assertFailsWith<EOFException> { source.readLineStrict(3) }
+        assertEquals("line", source.readLineStrict(4))
         assertTrue(source.exhausted())
 
-        sink.writeUtf8("line\n")
+        sink.writeString("line\n")
         sink.flush()
-        assertEquals("line", source.readUtf8LineStrict(5))
+        assertEquals("line", source.readLineStrict(5))
         assertTrue(source.exhausted())
     }
 
@@ -1536,4 +1539,170 @@ abstract class AbstractBufferedSourceTest internal constructor(
         }
         assertTrue(source.request(7))
     }
+
+    @Test
+    fun readByteString() {
+        with(sink) {
+            writeString("abcd")
+            writeString("e".repeat(Segment.SIZE))
+            emit()
+        }
+        assertEquals("abcd" + "e".repeat(Segment.SIZE), source.readByteString().decodeToString())
+    }
+
+    @Test
+    fun readByteStringPartial() {
+        with(sink) {
+            writeString("abcd")
+            writeString("e".repeat(Segment.SIZE))
+            emit()
+        }
+        assertEquals("abc", source.readByteString(3).decodeToString())
+        assertEquals("d", source.readString(1))
+    }
+
+    @Test
+    fun readByteStringTooShortThrows() {
+        sink.writeString("abc")
+        sink.emit()
+        assertFailsWith<EOFException> { source.readByteString(4) }
+
+        assertEquals("abc", source.readString()) // The read shouldn't consume any data.
+    }
+
+    @Test
+    fun indexOfByteString() {
+        assertEquals(-1, source.indexOf("flop".encodeToByteString()))
+
+        sink.writeString("flip flop")
+        sink.emit()
+        assertEquals(5, source.indexOf("flop".encodeToByteString()))
+        source.readString() // Clear stream.
+
+        // Make sure we backtrack and resume searching after partial match.
+        sink.writeString("hi hi hi hey")
+        sink.emit()
+        assertEquals(3, source.indexOf("hi hi hey".encodeToByteString()))
+    }
+
+    @Test
+    fun indexOfByteStringAtSegmentBoundary() {
+        sink.writeString("a".repeat(Segment.SIZE - 1))
+        sink.writeString("bcd")
+        sink.emit()
+        assertEquals(
+            (Segment.SIZE - 3).toLong(),
+            source.indexOf("aabc".encodeToByteString(), (Segment.SIZE - 4).toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE - 3).toLong(),
+            source.indexOf("aabc".encodeToByteString(), (Segment.SIZE - 3).toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE - 2).toLong(),
+            source.indexOf("abcd".encodeToByteString(), (Segment.SIZE - 2).toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE - 2).toLong(),
+            source.indexOf("abc".encodeToByteString(), (Segment.SIZE - 2).toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE - 2).toLong(),
+            source.indexOf("abc".encodeToByteString(), (Segment.SIZE - 2).toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE - 2).toLong(),
+            source.indexOf("ab".encodeToByteString(), (Segment.SIZE - 2).toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE - 2).toLong(),
+            source.indexOf("a".encodeToByteString(), (Segment.SIZE - 2).toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE - 1).toLong(),
+            source.indexOf("bc".encodeToByteString(), (Segment.SIZE - 2).toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE - 1).toLong(),
+            source.indexOf("b".encodeToByteString(), (Segment.SIZE - 2).toLong()),
+        )
+        assertEquals(
+            Segment.SIZE.toLong(),
+            source.indexOf("c".encodeToByteString(), (Segment.SIZE - 2).toLong()),
+        )
+        assertEquals(
+            Segment.SIZE.toLong(),
+            source.indexOf("c".encodeToByteString(), Segment.SIZE.toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE + 1).toLong(),
+            source.indexOf("d".encodeToByteString(), (Segment.SIZE - 2).toLong()),
+        )
+        assertEquals(
+            (Segment.SIZE + 1).toLong(),
+            source.indexOf("d".encodeToByteString(), (Segment.SIZE + 1).toLong()),
+        )
+    }
+
+    @Test
+    fun indexOfDoesNotWrapAround() {
+        sink.writeString("a".repeat(Segment.SIZE - 1))
+        sink.writeString("bcd")
+        sink.emit()
+        assertEquals(-1, source.indexOf("abcda".encodeToByteString(), (Segment.SIZE - 3).toLong()))
+    }
+
+    @Test
+    fun indexOfByteStringWithOffset() {
+        assertEquals(-1, source.indexOf("flop".encodeToByteString(), 1))
+
+        sink.writeString("flop flip flop")
+        sink.emit()
+        assertEquals(10, source.indexOf("flop".encodeToByteString(), 1))
+        source.readString() // Clear stream
+
+        // Make sure we backtrack and resume searching after partial match.
+        sink.writeString("hi hi hi hi hey")
+        sink.emit()
+        assertEquals(6, source.indexOf("hi hi hey".encodeToByteString(), 1))
+    }
+
+    @Test
+    fun indexOfEmptyByteString() {
+        assertEquals(0, source.indexOf(ByteString()))
+
+        sink.writeString("blablabla")
+        sink.emit()
+        assertEquals(0, source.indexOf(ByteString()))
+    }
+
+    @Test
+    fun indexOfByteStringInvalidArgumentsThrows() {
+        assertFailsWith<IllegalArgumentException> {
+            source.indexOf("hi".encodeToByteString(), -1)
+        }
+    }
+
+    /**
+     * 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.writeString("a".repeat(Segment.SIZE * 2 - 3))
+        sink.writeString("bcdefg")
+        sink.emit()
+        assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("ab".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abc".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abcd".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abcde".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abcdef".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 - 4).toLong(), source.indexOf("abcdefg".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 - 3).toLong(), source.indexOf("bcdefg".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 - 2).toLong(), source.indexOf("cdefg".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 - 1).toLong(), source.indexOf("defg".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2).toLong(), source.indexOf("efg".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 + 1).toLong(), source.indexOf("fg".encodeToByteString()))
+        assertEquals((Segment.SIZE * 2 + 2).toLong(), source.indexOf("g".encodeToByteString()))
+    }
 }
diff --git a/core/common/test/CommonBufferTest.kt b/core/common/test/CommonBufferTest.kt
index abd25789..d450557a 100644
--- a/core/common/test/CommonBufferTest.kt
+++ b/core/common/test/CommonBufferTest.kt
@@ -20,6 +20,8 @@
  */
 package kotlinx.io
 
+import kotlinx.io.bytestring.ByteString
+import kotlinx.io.bytestring.encodeToByteString
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -35,16 +37,16 @@ class CommonBufferTest {
     @Test
     fun readAndWriteUtf8() {
         val buffer = Buffer()
-        buffer.writeUtf8("ab")
+        buffer.writeString("ab")
         assertEquals(2, buffer.size)
-        buffer.writeUtf8("cdef")
+        buffer.writeString("cdef")
         assertEquals(6, buffer.size)
-        assertEquals("abcd", buffer.readUtf8(4))
+        assertEquals("abcd", buffer.readString(4))
         assertEquals(2, buffer.size)
-        assertEquals("ef", buffer.readUtf8(2))
+        assertEquals("ef", buffer.readString(2))
         assertEquals(0, buffer.size)
         assertFailsWith<EOFException> {
-            buffer.readUtf8(1)
+            buffer.readString(1)
         }
     }
 
@@ -52,25 +54,30 @@ class CommonBufferTest {
     fun bufferToString() {
         assertEquals("Buffer(size=0)", Buffer().toString())
 
-        assertEquals("Buffer(size=10 hex=610d0a620a630d645c65)",
-            Buffer().also { it.writeUtf8("a\r\nb\nc\rd\\e") }.toString()
+        assertEquals(
+            "Buffer(size=10 hex=610d0a620a630d645c65)",
+            Buffer().also { it.writeString("a\r\nb\nc\rd\\e") }.toString()
         )
 
-        assertEquals("Buffer(size=11 hex=547972616e6e6f73617572)",
-            Buffer().also { it.writeUtf8("Tyrannosaur") }.toString()
+        assertEquals(
+            "Buffer(size=11 hex=547972616e6e6f73617572)",
+            Buffer().also { it.writeString("Tyrannosaur") }.toString()
         )
 
-        assertEquals("Buffer(size=16 hex=74c999cb8872616ec999cb8c73c3b472)",
+        assertEquals(
+            "Buffer(size=16 hex=74c999cb8872616ec999cb8c73c3b472)",
             Buffer().also { it.write("74c999cb8872616ec999cb8c73c3b472".decodeHex()) }.toString()
         )
 
-        assertEquals("Buffer(size=64 hex=00000000000000000000000000000000000000000000000000000000000000000000000" +
-                "000000000000000000000000000000000000000000000000000000000)",
+        assertEquals(
+            "Buffer(size=64 hex=00000000000000000000000000000000000000000000000000000000000000000000000" +
+                    "000000000000000000000000000000000000000000000000000000000)",
             Buffer().also { it.write(ByteArray(64)) }.toString()
         )
 
-        assertEquals("Buffer(size=66 hex=000000000000000000000000000000000000000000000000000000000000" +
-                "00000000000000000000000000000000000000000000000000000000000000000000…)",
+        assertEquals(
+            "Buffer(size=66 hex=000000000000000000000000000000000000000000000000000000000000" +
+                    "00000000000000000000000000000000000000000000000000000000000000000000…)",
             Buffer().also { it.write(ByteArray(66)) }.toString()
         )
     }
@@ -78,19 +85,19 @@ class CommonBufferTest {
     @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
+        buffer.writeString('a'.repeat(1000))
+        buffer.writeString('b'.repeat(2500))
+        buffer.writeString('c'.repeat(5000))
+        buffer.writeString('d'.repeat(10000))
+        buffer.writeString('e'.repeat(25000))
+        buffer.writeString('f'.repeat(50000))
+
+        assertEquals('a'.repeat(999), buffer.readString(999)) // a...a
+        assertEquals("a" + 'b'.repeat(2500) + "c", buffer.readString(2502)) // ab...bc
+        assertEquals('c'.repeat(4998), buffer.readString(4998)) // c...c
+        assertEquals("c" + 'd'.repeat(10000) + "e", buffer.readString(10002)) // cd...de
+        assertEquals('e'.repeat(24998), buffer.readString(24998)) // e...e
+        assertEquals("e" + 'f'.repeat(50000), buffer.readString(50001)) // ef...f
         assertEquals(0, buffer.size)
     }
 
@@ -152,12 +159,12 @@ class CommonBufferTest {
         val buffer = Buffer()
         for (s in contents) {
             val source = Buffer()
-            source.writeUtf8(s)
+            source.writeString(s)
             buffer.transferFrom(source)
             expected.append(s)
         }
         val segmentSizes = segmentSizes(buffer)
-        assertEquals(expected.toString(), buffer.readUtf8(expected.length.toLong()))
+        assertEquals(expected.toString(), buffer.readString(expected.length.toLong()))
         return segmentSizes
     }
 
@@ -167,10 +174,10 @@ class CommonBufferTest {
         val writeSize = Segment.SIZE / 2 + 1
 
         val sink = Buffer()
-        sink.writeUtf8('b'.repeat(Segment.SIZE - 10))
+        sink.writeString('b'.repeat(Segment.SIZE - 10))
 
         val source = Buffer()
-        source.writeUtf8('a'.repeat(Segment.SIZE * 2))
+        source.writeString('a'.repeat(Segment.SIZE * 2))
         sink.write(source, writeSize.toLong())
 
         assertEquals(listOf(Segment.SIZE - 10, writeSize), segmentSizes(sink))
@@ -183,10 +190,10 @@ class CommonBufferTest {
         val writeSize = Segment.SIZE / 2 - 1
 
         val sink = Buffer()
-        sink.writeUtf8('b'.repeat(Segment.SIZE - 10))
+        sink.writeString('b'.repeat(Segment.SIZE - 10))
 
         val source = Buffer()
-        source.writeUtf8('a'.repeat(Segment.SIZE * 2))
+        source.writeString('a'.repeat(Segment.SIZE * 2))
         sink.write(source, writeSize.toLong())
 
         assertEquals(listOf(Segment.SIZE - 10, writeSize), segmentSizes(sink))
@@ -196,10 +203,10 @@ class CommonBufferTest {
     @Test
     fun writePrefixDoesntSplit() {
         val sink = Buffer()
-        sink.writeUtf8('b'.repeat(10))
+        sink.writeString('b'.repeat(10))
 
         val source = Buffer()
-        source.writeUtf8('a'.repeat(Segment.SIZE * 2))
+        source.writeString('a'.repeat(Segment.SIZE * 2))
         sink.write(source, 20)
 
         assertEquals(listOf(30), segmentSizes(sink))
@@ -211,11 +218,11 @@ class CommonBufferTest {
     @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
+        sink.writeString('b'.repeat(Segment.SIZE - 10)) // limit = size - 10
+        sink.readString((Segment.SIZE - 20).toLong()) // pos = size = 20
 
         val source = Buffer()
-        source.writeUtf8('a'.repeat(Segment.SIZE * 2))
+        source.writeString('a'.repeat(Segment.SIZE * 2))
         sink.write(source, 20)
 
         assertEquals(listOf(30), segmentSizes(sink))
@@ -235,39 +242,39 @@ class CommonBufferTest {
     @Test
     fun moveAllRequestedBytesWithRead() {
         val sink = Buffer()
-        sink.writeUtf8('a'.repeat(10))
+        sink.writeString('a'.repeat(10))
 
         val source = Buffer()
-        source.writeUtf8('b'.repeat(15))
+        source.writeString('b'.repeat(15))
 
         assertEquals(10, source.readAtMostTo(sink, 10))
         assertEquals(20, sink.size)
         assertEquals(5, source.size)
-        assertEquals('a'.repeat(10) + 'b'.repeat(10), sink.readUtf8(20))
+        assertEquals('a'.repeat(10) + 'b'.repeat(10), sink.readString(20))
     }
 
     @Test
     fun moveFewerThanRequestedBytesWithRead() {
         val sink = Buffer()
-        sink.writeUtf8('a'.repeat(10))
+        sink.writeString('a'.repeat(10))
 
         val source = Buffer()
-        source.writeUtf8('b'.repeat(20))
+        source.writeString('b'.repeat(20))
 
         assertEquals(20, source.readAtMostTo(sink, 25))
         assertEquals(30, sink.size)
         assertEquals(0, source.size)
-        assertEquals('a'.repeat(10) + 'b'.repeat(20), sink.readUtf8(30))
+        assertEquals('a'.repeat(10) + 'b'.repeat(20), sink.readString(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))
+        buffer.writeString('a'.repeat(halfSegment))
+        buffer.writeString('b'.repeat(halfSegment))
+        buffer.writeString('c'.repeat(halfSegment))
+        buffer.writeString('d'.repeat(halfSegment))
         assertEquals(0, buffer.indexOf('a'.code.toByte(), 0))
         assertEquals((halfSegment - 1).toLong(), buffer.indexOf('a'.code.toByte(), (halfSegment - 1).toLong()))
         assertEquals(halfSegment.toLong(), buffer.indexOf('b'.code.toByte(), (halfSegment - 1).toLong()))
@@ -281,9 +288,9 @@ class CommonBufferTest {
     @Test
     fun byteAt() {
         val buffer = Buffer()
-        buffer.writeUtf8("a")
-        buffer.writeUtf8('b'.repeat(Segment.SIZE))
-        buffer.writeUtf8("c")
+        buffer.writeString("a")
+        buffer.writeString('b'.repeat(Segment.SIZE))
+        buffer.writeString("c")
         assertEquals('a'.code.toLong(), buffer[0].toLong())
         assertEquals('a'.code.toLong(), buffer[0].toLong()) // getByte doesn't mutate!
         assertEquals('c'.code.toLong(), buffer[buffer.size - 1].toLong())
@@ -311,21 +318,21 @@ class CommonBufferTest {
     fun writePrefixToEmptyBuffer() {
         val sink = Buffer()
         val source = Buffer()
-        source.writeUtf8("abcd")
+        source.writeString("abcd")
         sink.write(source, 2)
-        assertEquals("ab", sink.readUtf8(2))
+        assertEquals("ab", sink.readString(2))
     }
 
     // Buffer don't override equals and hashCode
     @Test
     fun equalsAndHashCode() {
-        val a = Buffer().also { it.writeUtf8("dog") }
+        val a = Buffer().also { it.writeString("dog") }
         assertEquals(a, a)
 
-        val b = Buffer().also { it.writeUtf8("hotdog") }
+        val b = Buffer().also { it.writeString("hotdog") }
         assertTrue(a != b)
 
-        b.readUtf8(3) // Leaves b containing 'dog'.
+        b.readString(3) // Leaves b containing 'dog'.
         assertTrue(a != b)
     }
 
@@ -336,14 +343,14 @@ class CommonBufferTest {
     @Test
     fun readAllWritesAllSegmentsAtOnce() {
         val write1 = Buffer()
-        write1.writeUtf8(
+        write1.writeString(
             'a'.repeat(Segment.SIZE) +
                     'b'.repeat(Segment.SIZE) +
                     'c'.repeat(Segment.SIZE)
         )
 
         val source = Buffer()
-        source.writeUtf8(
+        source.writeString(
             'a'.repeat(Segment.SIZE) +
                     'b'.repeat(Segment.SIZE) +
                     'c'.repeat(Segment.SIZE)
@@ -358,60 +365,60 @@ class CommonBufferTest {
 
     @Test
     fun writeAllMultipleSegments() {
-        val source = Buffer().also { it.writeUtf8('a'.repeat(Segment.SIZE * 3)) }
+        val source = Buffer().also { it.writeString('a'.repeat(Segment.SIZE * 3)) }
         val sink = Buffer()
 
         assertEquals((Segment.SIZE * 3).toLong(), sink.transferFrom(source))
         assertEquals(0, source.size)
-        assertEquals('a'.repeat(Segment.SIZE * 3), sink.readUtf8())
+        assertEquals('a'.repeat(Segment.SIZE * 3), sink.readString())
     }
 
     @Test
     fun copyTo() {
         val source = Buffer()
-        source.writeUtf8("party")
+        source.writeString("party")
 
         val target = Buffer()
         source.copyTo(target, startIndex = 1, endIndex = 4)
 
-        assertEquals("art", target.readUtf8())
-        assertEquals("party", source.readUtf8())
+        assertEquals("art", target.readString())
+        assertEquals("party", source.readString())
     }
 
     @Test
     fun copyToAll() {
         val source = Buffer()
-        source.writeUtf8("hello")
+        source.writeString("hello")
 
         val target = Buffer()
         source.copyTo(target)
 
-        assertEquals("hello", source.readUtf8())
-        assertEquals("hello", target.readUtf8())
+        assertEquals("hello", source.readString())
+        assertEquals("hello", target.readString())
     }
 
     @Test
     fun copyToWithOnlyStartIndex() {
         val source = Buffer()
-        source.writeUtf8("hello")
+        source.writeString("hello")
 
         val target = Buffer()
         source.copyTo(target, startIndex = 1)
 
-        assertEquals("hello", source.readUtf8())
-        assertEquals("ello", target.readUtf8())
+        assertEquals("hello", source.readString())
+        assertEquals("ello", target.readString())
     }
 
     @Test
     fun copyToWithOnlyEndIndex() {
         val source = Buffer()
-        source.writeUtf8("hello")
+        source.writeString("hello")
 
         val target = Buffer()
         source.copyTo(target, endIndex = 1)
 
-        assertEquals("hello", source.readUtf8())
-        assertEquals("h", target.readUtf8())
+        assertEquals("hello", source.readString())
+        assertEquals("h", target.readString())
     }
 
     @Test
@@ -422,18 +429,18 @@ class CommonBufferTest {
         val ds = 'd'.repeat(Segment.SIZE)
 
         val source = Buffer()
-        source.writeUtf8(aStr)
-        source.writeUtf8(bs)
-        source.writeUtf8(cs)
+        source.writeString(aStr)
+        source.writeString(bs)
+        source.writeString(cs)
 
         val target = Buffer()
-        target.writeUtf8(ds)
+        target.writeString(ds)
 
         source.copyTo(
             target, startIndex = aStr.length.toLong(),
             endIndex = aStr.length.toLong() + (bs.length + cs.length).toLong()
         )
-        assertEquals(ds + bs + cs, target.readUtf8())
+        assertEquals(ds + bs + cs, target.readString())
     }
 
     @Test
@@ -444,18 +451,18 @@ class CommonBufferTest {
         val ds = 'd'.repeat(Segment.SIZE + 8)
 
         val source = Buffer()
-        source.writeUtf8(aStr)
-        source.writeUtf8(bs)
-        source.writeUtf8(cs)
+        source.writeString(aStr)
+        source.writeString(bs)
+        source.writeString(cs)
 
         val target = Buffer()
-        target.writeUtf8(ds)
+        target.writeString(ds)
 
         source.copyTo(
             target, startIndex = aStr.length.toLong(),
             endIndex = aStr.length.toLong() + (bs.length + cs.length).toLong()
         )
-        assertEquals(ds + bs + cs, target.readUtf8())
+        assertEquals(ds + bs + cs, target.readString())
     }
 
     @Test
@@ -464,29 +471,29 @@ class CommonBufferTest {
         val bs = 'b'.repeat(Segment.SIZE)
 
         val source = Buffer()
-        source.writeUtf8(aStr)
-        source.writeUtf8(bs)
+        source.writeString(aStr)
+        source.writeString(bs)
 
         source.copyTo(source, startIndex = 0, endIndex = source.size)
-        assertEquals(aStr + bs + aStr + bs, source.readUtf8())
+        assertEquals(aStr + bs + aStr + bs, source.readString())
     }
 
     @Test
     fun copyToEmptySource() {
         val source = Buffer()
-        val target = Buffer().also { it.writeUtf8("aaa") }
+        val target = Buffer().also { it.writeString("aaa") }
         source.copyTo(target, startIndex = 0L, endIndex = 0L)
-        assertEquals("", source.readUtf8())
-        assertEquals("aaa", target.readUtf8())
+        assertEquals("", source.readString())
+        assertEquals("aaa", target.readString())
     }
 
     @Test
     fun copyToEmptyTarget() {
-        val source = Buffer().also { it.writeUtf8("aaa") }
+        val source = Buffer().also { it.writeString("aaa") }
         val target = Buffer()
         source.copyTo(target, startIndex = 0L, endIndex = 3L)
-        assertEquals("aaa", source.readUtf8())
-        assertEquals("aaa", target.readUtf8())
+        assertEquals("aaa", source.readString())
+        assertEquals("aaa", target.readString())
     }
 
     @Test
@@ -498,14 +505,14 @@ class CommonBufferTest {
     @Test
     fun completeSegmentByteCountOnBufferWithFullSegments() {
         val buffer = Buffer()
-        buffer.writeUtf8("a".repeat(Segment.SIZE * 4))
+        buffer.writeString("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))
+        buffer.writeString("a".repeat(Segment.SIZE * 4 - 10))
         assertEquals((Segment.SIZE * 3).toLong(), buffer.completeSegmentByteCount())
     }
 
@@ -513,53 +520,53 @@ class CommonBufferTest {
     fun cloneDoesNotObserveWritesToOriginal() {
         val original = Buffer()
         val clone: Buffer = original.copy()
-        original.writeUtf8("abc")
+        original.writeString("abc")
         assertEquals(0, clone.size)
     }
 
     @Test
     fun cloneDoesNotObserveReadsFromOriginal() {
         val original = Buffer()
-        original.writeUtf8("abc")
+        original.writeString("abc")
         val clone: Buffer = original.copy()
-        assertEquals("abc", original.readUtf8(3))
+        assertEquals("abc", original.readString(3))
         assertEquals(3, clone.size)
-        assertEquals("ab", clone.readUtf8(2))
+        assertEquals("ab", clone.readString(2))
     }
 
     @Test
     fun originalDoesNotObserveWritesToClone() {
         val original = Buffer()
         val clone: Buffer = original.copy()
-        clone.writeUtf8("abc")
+        clone.writeString("abc")
         assertEquals(0, original.size)
     }
 
     @Test
     fun originalDoesNotObserveReadsFromClone() {
         val original = Buffer()
-        original.writeUtf8("abc")
+        original.writeString("abc")
         val clone: Buffer = original.copy()
-        assertEquals("abc", clone.readUtf8(3))
+        assertEquals("abc", clone.readString(3))
         assertEquals(3, original.size)
-        assertEquals("ab", original.readUtf8(2))
+        assertEquals("ab", original.readString(2))
     }
 
     @Test
     fun cloneMultipleSegments() {
         val original = Buffer()
-        original.writeUtf8("a".repeat(SEGMENT_SIZE * 3))
+        original.writeString("a".repeat(SEGMENT_SIZE * 3))
         val clone: Buffer = original.copy()
-        original.writeUtf8("b".repeat(SEGMENT_SIZE * 3))
-        clone.writeUtf8("c".repeat(SEGMENT_SIZE * 3))
+        original.writeString("b".repeat(SEGMENT_SIZE * 3))
+        clone.writeString("c".repeat(SEGMENT_SIZE * 3))
 
         assertEquals(
             "a".repeat(SEGMENT_SIZE * 3) + "b".repeat(SEGMENT_SIZE * 3),
-            original.readUtf8((SEGMENT_SIZE * 6).toLong())
+            original.readString((SEGMENT_SIZE * 6).toLong())
         )
         assertEquals(
             "a".repeat(SEGMENT_SIZE * 3) + "c".repeat(SEGMENT_SIZE * 3),
-            clone.readUtf8((SEGMENT_SIZE * 6).toLong())
+            clone.readString((SEGMENT_SIZE * 6).toLong())
         )
     }
 
@@ -584,4 +591,14 @@ class CommonBufferTest {
         copy.transferTo(buffer)
         assertArrayEquals(byteArrayOf(42, 42), buffer.readByteArray())
     }
+
+    @Test
+    fun snapshot() {
+        val buffer = Buffer()
+        assertEquals(ByteString(), buffer.snapshot())
+        buffer.writeString("hello")
+        assertEquals("hello".encodeToByteString(), buffer.snapshot())
+        buffer.clear()
+        assertEquals(ByteString(), buffer.snapshot())
+    }
 }
diff --git a/core/common/test/CommonPlatformTest.kt b/core/common/test/CommonPlatformTest.kt
index 79d6734d..2cf8faa2 100644
--- a/core/common/test/CommonPlatformTest.kt
+++ b/core/common/test/CommonPlatformTest.kt
@@ -27,9 +27,9 @@ import kotlin.test.assertEquals
 class CommonPlatformTest {
     @Test
     fun sourceBuffer() {
-        val source = Buffer().also { it.writeUtf8("a") }
+        val source = Buffer().also { it.writeString("a") }
         val buffered = (source as RawSource).buffered()
-        assertEquals(buffered.readUtf8(), "a")
+        assertEquals(buffered.readString(), "a")
         assertEquals(source.size, 0L)
     }
 
@@ -37,7 +37,7 @@ class CommonPlatformTest {
     fun sinkBuffer() {
         val sink = Buffer()
         val buffered = (sink as RawSink).buffered()
-        buffered.writeUtf8("a")
+        buffered.writeString("a")
         assertEquals(sink.size, 0L)
         buffered.flush()
         assertEquals(sink.size, 1L)
@@ -45,6 +45,6 @@ class CommonPlatformTest {
 
     @Test
     fun discardingSinkTest() {
-        discardingSink().write(Buffer().also { it.writeUtf8("a") }, 1L)
+        discardingSink().write(Buffer().also { it.writeString("a") }, 1L)
     }
 }
diff --git a/core/common/test/CommonRealSinkTest.kt b/core/common/test/CommonRealSinkTest.kt
index 2ee1a645..f6b454e9 100644
--- a/core/common/test/CommonRealSinkTest.kt
+++ b/core/common/test/CommonRealSinkTest.kt
@@ -35,7 +35,7 @@ class CommonRealSinkTest {
     fun bufferedSinkEmitsTailWhenItIsComplete() {
         val sink = Buffer()
         val bufferedSink = (sink as RawSink).buffered()
-        bufferedSink.writeUtf8("a".repeat(Segment.SIZE - 1))
+        bufferedSink.writeString("a".repeat(Segment.SIZE - 1))
         assertEquals(0, sink.size)
         bufferedSink.writeByte(0)
         assertEquals(Segment.SIZE.toLong(), sink.size)
@@ -46,7 +46,7 @@ class CommonRealSinkTest {
     fun bufferedSinkEmitMultipleSegments() {
         val sink = Buffer()
         val bufferedSink = (sink as RawSink).buffered()
-        bufferedSink.writeUtf8("a".repeat(Segment.SIZE * 4 - 1))
+        bufferedSink.writeString("a".repeat(Segment.SIZE * 4 - 1))
         assertEquals(Segment.SIZE.toLong() * 3L, sink.size)
         assertEquals(Segment.SIZE.toLong() - 1L, bufferedSink.buffer.size)
     }
@@ -66,7 +66,7 @@ class CommonRealSinkTest {
     fun bytesEmittedToSinkWithFlush() {
         val sink = Buffer()
         val bufferedSink = (sink as RawSink).buffered()
-        bufferedSink.writeUtf8("abc")
+        bufferedSink.writeString("abc")
         bufferedSink.flush()
         assertEquals(3, sink.size)
     }
@@ -75,7 +75,7 @@ class CommonRealSinkTest {
     fun bytesNotEmittedToSinkWithoutFlush() {
         val sink = Buffer()
         val bufferedSink = (sink as RawSink).buffered()
-        bufferedSink.writeUtf8("abc")
+        bufferedSink.writeString("abc")
         assertEquals(0, sink.size)
     }
 
@@ -83,7 +83,7 @@ class CommonRealSinkTest {
     fun bytesEmittedToSinkWithEmit() {
         val sink = Buffer()
         val bufferedSink = (sink as RawSink).buffered()
-        bufferedSink.writeUtf8("abc")
+        bufferedSink.writeString("abc")
         bufferedSink.emit()
         assertEquals(3, sink.size)
     }
@@ -92,7 +92,7 @@ class CommonRealSinkTest {
     fun completeSegmentsEmitted() {
         val sink = Buffer()
         val bufferedSink = (sink as RawSink).buffered()
-        bufferedSink.writeUtf8("a".repeat(Segment.SIZE * 3))
+        bufferedSink.writeString("a".repeat(Segment.SIZE * 3))
         assertEquals(Segment.SIZE.toLong() * 3L, sink.size)
     }
 
@@ -100,7 +100,7 @@ class CommonRealSinkTest {
     fun incompleteSegmentsNotEmitted() {
         val sink = Buffer()
         val bufferedSink = (sink as RawSink).buffered()
-        bufferedSink.writeUtf8("a".repeat(Segment.SIZE * 3 - 1))
+        bufferedSink.writeString("a".repeat(Segment.SIZE * 3 - 1))
         assertEquals(Segment.SIZE.toLong() * 2L, sink.size)
     }
 
@@ -164,11 +164,11 @@ class CommonRealSinkTest {
         val mockSink = MockSink()
         val bufferedSink = mockSink.buffered()
 
-        bufferedSink.buffer.writeUtf8("abc")
-        assertEquals(3, bufferedSink.transferFrom(Buffer().also { it.writeUtf8("def") }))
+        bufferedSink.buffer.writeString("abc")
+        assertEquals(3, bufferedSink.transferFrom(Buffer().also { it.writeString("def") }))
 
         assertEquals(6, bufferedSink.buffer.size)
-        assertEquals("abcdef", bufferedSink.buffer.readUtf8(6))
+        assertEquals("abcdef", bufferedSink.buffer.readString(6))
         mockSink.assertLog() // No writes.
     }
 
@@ -184,12 +184,12 @@ class CommonRealSinkTest {
 
     @Test
     fun writeAllWritesOneSegmentAtATime() {
-        val write1 = Buffer().also { it.writeUtf8("a".repeat(Segment.SIZE)) }
-        val write2 = Buffer().also { it.writeUtf8("b".repeat(Segment.SIZE)) }
-        val write3 = Buffer().also { it.writeUtf8("c".repeat(Segment.SIZE)) }
+        val write1 = Buffer().also { it.writeString("a".repeat(Segment.SIZE)) }
+        val write2 = Buffer().also { it.writeString("b".repeat(Segment.SIZE)) }
+        val write3 = Buffer().also { it.writeString("c".repeat(Segment.SIZE)) }
 
         val source = Buffer()
-        source.writeUtf8(
+        source.writeString(
             "${"a".repeat(Segment.SIZE)}${"b".repeat(Segment.SIZE)}${"c".repeat(Segment.SIZE)}"
         )
 
diff --git a/core/common/test/CommonRealSourceTest.kt b/core/common/test/CommonRealSourceTest.kt
index 9f056b28..b345516f 100644
--- a/core/common/test/CommonRealSourceTest.kt
+++ b/core/common/test/CommonRealSourceTest.kt
@@ -33,7 +33,7 @@ import kotlin.test.assertFailsWith
 class CommonRealSourceTest {
     @Test
     fun indexOfStopsReadingAtLimit() {
-        val buffer = Buffer().also { it.writeUtf8("abcdef") }
+        val buffer = Buffer().also { it.writeString("abcdef") }
         val bufferedSource = (
                 object : RawSource by buffer {
                     override fun readAtMostTo(sink: Buffer, byteCount: Long): Long {
@@ -50,10 +50,10 @@ class CommonRealSourceTest {
     @Test
     fun requireTracksBufferFirst() {
         val source = Buffer()
-        source.writeUtf8("bb")
+        source.writeString("bb")
 
         val bufferedSource = (source as RawSource).buffered()
-        bufferedSource.buffer.writeUtf8("aa")
+        bufferedSource.buffer.writeString("aa")
 
         bufferedSource.require(2)
         assertEquals(2, bufferedSource.buffer.size)
@@ -63,19 +63,19 @@ class CommonRealSourceTest {
     @Test
     fun requireIncludesBufferBytes() {
         val source = Buffer()
-        source.writeUtf8("b")
+        source.writeString("b")
 
         val bufferedSource = (source as RawSource).buffered()
-        bufferedSource.buffer.writeUtf8("a")
+        bufferedSource.buffer.writeString("a")
 
         bufferedSource.require(2)
-        assertEquals("ab", bufferedSource.buffer.readUtf8(2))
+        assertEquals("ab", bufferedSource.buffer.readString(2))
     }
 
     @Test
     fun requireInsufficientData() {
         val source = Buffer()
-        source.writeUtf8("a")
+        source.writeString("a")
 
         val bufferedSource = (source as RawSource).buffered()
 
@@ -87,8 +87,8 @@ class CommonRealSourceTest {
     @Test
     fun requireReadsOneSegmentAtATime() {
         val source = Buffer()
-        source.writeUtf8("a".repeat(Segment.SIZE))
-        source.writeUtf8("b".repeat(Segment.SIZE))
+        source.writeString("a".repeat(Segment.SIZE))
+        source.writeString("b".repeat(Segment.SIZE))
 
         val bufferedSource = (source as RawSource).buffered()
 
@@ -100,8 +100,8 @@ class CommonRealSourceTest {
     @Test
     fun skipReadsOneSegmentAtATime() {
         val source = Buffer()
-        source.writeUtf8("a".repeat(Segment.SIZE))
-        source.writeUtf8("b".repeat(Segment.SIZE))
+        source.writeString("a".repeat(Segment.SIZE))
+        source.writeString("b".repeat(Segment.SIZE))
         val bufferedSource = (source as RawSource).buffered()
         bufferedSource.skip(2)
         assertEquals(Segment.SIZE.toLong(), source.size)
@@ -111,10 +111,10 @@ class CommonRealSourceTest {
     @Test
     fun skipTracksBufferFirst() {
         val source = Buffer()
-        source.writeUtf8("bb")
+        source.writeString("bb")
 
         val bufferedSource = (source as RawSource).buffered()
-        bufferedSource.buffer.writeUtf8("aa")
+        bufferedSource.buffer.writeString("aa")
 
         bufferedSource.skip(2)
         assertEquals(0, bufferedSource.buffer.size)
@@ -143,12 +143,12 @@ class CommonRealSourceTest {
      */
     @Test
     fun transferToReadsOneSegmentAtATime() {
-        val write1 = Buffer().also { it.writeUtf8("a".repeat(Segment.SIZE)) }
-        val write2 = Buffer().also { it.writeUtf8("b".repeat(Segment.SIZE)) }
-        val write3 = Buffer().also { it.writeUtf8("c".repeat(Segment.SIZE)) }
+        val write1 = Buffer().also { it.writeString("a".repeat(Segment.SIZE)) }
+        val write2 = Buffer().also { it.writeString("b".repeat(Segment.SIZE)) }
+        val write3 = Buffer().also { it.writeString("c".repeat(Segment.SIZE)) }
 
         val source = Buffer()
-        source.writeUtf8(
+        source.writeString(
             "${"a".repeat(Segment.SIZE)}${"b".repeat(Segment.SIZE)}${"c".repeat(Segment.SIZE)}"
         )
 
diff --git a/core/common/test/Utf8Test.kt b/core/common/test/Utf8Test.kt
index e526dcb9..d040f8a0 100644
--- a/core/common/test/Utf8Test.kt
+++ b/core/common/test/Utf8Test.kt
@@ -332,7 +332,7 @@ class Utf8Test {
     }
 
     private fun Buffer.assertUtf8StringEncoded(expectedHex: String, string: String) {
-        writeUtf8(string)
+        writeString(string)
         assertArrayEquals(expectedHex.decodeHex(), readByteArray())
     }
 
diff --git a/core/common/test/files/SmokeFileTest.kt b/core/common/test/files/SmokeFileTest.kt
index 57336091..f47a12ad 100644
--- a/core/common/test/files/SmokeFileTest.kt
+++ b/core/common/test/files/SmokeFileTest.kt
@@ -30,11 +30,11 @@ class SmokeFileTest {
         val path = Path(tempFile!!)
 
         path.sink().use {
-            it.writeUtf8("example")
+            it.writeString("example")
         }
 
         path.source().use {
-            assertEquals("example", it.readUtf8Line())
+            assertEquals("example", it.readLine())
         }
     }
 
diff --git a/core/jvm/test/AbstractSinkTestJVM.kt b/core/jvm/test/AbstractSinkTestJVM.kt
index d383c141..c143c151 100644
--- a/core/jvm/test/AbstractSinkTestJVM.kt
+++ b/core/jvm/test/AbstractSinkTestJVM.kt
@@ -45,7 +45,7 @@ abstract class AbstractSinkTestJVM internal constructor(factory: SinkFactory) {
         out.write("b".repeat(9998).toByteArray(UTF_8))
         out.write('c'.code)
         out.flush()
-        assertEquals(("a" + "b".repeat(9998)) + "c", data.readUtf8())
+        assertEquals(("a" + "b".repeat(9998)) + "c", data.readString())
     }
 
     @Test
@@ -90,7 +90,7 @@ abstract class AbstractSinkTestJVM internal constructor(factory: SinkFactory) {
         assertEquals(expected.length, nioByteBuffer.position())
         assertEquals(expected.length, nioByteBuffer.limit())
         sink.flush()
-        assertEquals(expected, data.readUtf8())
+        assertEquals(expected, data.readString())
     }
 
     @Test
@@ -104,7 +104,7 @@ abstract class AbstractSinkTestJVM internal constructor(factory: SinkFactory) {
         assertEquals(expected.length, nioByteBuffer.position())
         assertEquals(expected.length, nioByteBuffer.limit())
         sink.flush()
-        assertEquals(expected, data.readUtf8())
+        assertEquals(expected, data.readString())
     }
 
     @Test
diff --git a/core/jvm/test/AbstractSourceTestJVM.kt b/core/jvm/test/AbstractSourceTestJVM.kt
index 5a3f31ef..dec531d6 100644
--- a/core/jvm/test/AbstractSourceTestJVM.kt
+++ b/core/jvm/test/AbstractSourceTestJVM.kt
@@ -50,7 +50,7 @@ abstract class AbstractSourceTestJVM(private val factory: SourceFactory) {
 
     @Test
     fun inputStream() {
-        sink.writeUtf8("abc")
+        sink.writeString("abc")
         sink.emit()
         val input: InputStream = source.asInputStream()
         val bytes = byteArrayOf('z'.code.toByte(), 'z'.code.toByte(), 'z'.code.toByte())
@@ -73,7 +73,7 @@ abstract class AbstractSourceTestJVM(private val factory: SourceFactory) {
 
     @Test
     fun inputStreamOffsetCount() {
-        sink.writeUtf8("abcde")
+        sink.writeString("abcde")
         sink.emit()
         val input: InputStream = source.asInputStream()
         val bytes =
@@ -90,12 +90,12 @@ abstract class AbstractSourceTestJVM(private val factory: SourceFactory) {
 
     @Test
     fun inputStreamSkip() {
-        sink.writeUtf8("abcde")
+        sink.writeString("abcde")
         sink.emit()
         val input: InputStream = source.asInputStream()
         assertEquals(4, input.skip(4))
         assertEquals('e'.code, input.read())
-        sink.writeUtf8("abcde")
+        sink.writeString("abcde")
         sink.emit()
         assertEquals(5, input.skip(10)) // Try to skip too much.
         assertEquals(0, input.skip(1)) // Try to skip when exhausted.
@@ -103,7 +103,7 @@ abstract class AbstractSourceTestJVM(private val factory: SourceFactory) {
 
     @Test
     fun inputStreamCharByChar() {
-        sink.writeUtf8("abc")
+        sink.writeString("abc")
         sink.emit()
         val input: InputStream = source.asInputStream()
         assertEquals('a'.code, input.read())
@@ -114,7 +114,7 @@ abstract class AbstractSourceTestJVM(private val factory: SourceFactory) {
 
     @Test
     fun inputStreamBounds() {
-        sink.writeUtf8("a".repeat(100))
+        sink.writeString("a".repeat(100))
         sink.emit()
         val input: InputStream = source.asInputStream()
         assertFailsWith<IllegalArgumentException> {
@@ -196,7 +196,7 @@ abstract class AbstractSourceTestJVM(private val factory: SourceFactory) {
     @Test
     fun readNioBuffer() {
         val expected = if (factory.isOneByteAtATime) "a" else "abcdefg"
-        sink.writeUtf8("abcdefg")
+        sink.writeString("abcdefg")
         sink.emit()
         val nioByteBuffer: ByteBuffer = ByteBuffer.allocate(1024)
         val byteCount: Int = source.readAtMostTo(nioByteBuffer)
@@ -213,7 +213,7 @@ abstract class AbstractSourceTestJVM(private val factory: SourceFactory) {
     @Test
     fun readLargeNioBufferOnlyReadsOneSegment() {
         val expected: String = if (factory.isOneByteAtATime) "a" else "a".repeat(SEGMENT_SIZE)
-        sink.writeUtf8("a".repeat(SEGMENT_SIZE * 4))
+        sink.writeString("a".repeat(SEGMENT_SIZE * 4))
         sink.emit()
         val nioByteBuffer: ByteBuffer = ByteBuffer.allocate(SEGMENT_SIZE * 3)
         val byteCount: Int = source.readAtMostTo(nioByteBuffer)
@@ -259,6 +259,6 @@ abstract class AbstractSourceTestJVM(private val factory: SourceFactory) {
         assertFailsWith<EOFException> {
             source.readString(4, Charsets.US_ASCII)
         }
-        assertEquals("abc", source.readUtf8()) // The read shouldn't consume any data.
+        assertEquals("abc", source.readString()) // The read shouldn't consume any data.
     }
 }
diff --git a/core/jvm/test/BufferTest.kt b/core/jvm/test/BufferTest.kt
index bab8a2cd..693cf67c 100644
--- a/core/jvm/test/BufferTest.kt
+++ b/core/jvm/test/BufferTest.kt
@@ -36,8 +36,8 @@ class BufferTest {
     @Test
     fun copyToSpanningSegments() {
         val source = Buffer()
-        source.writeUtf8("a".repeat(SEGMENT_SIZE * 2))
-        source.writeUtf8("b".repeat(SEGMENT_SIZE * 2))
+        source.writeString("a".repeat(SEGMENT_SIZE * 2))
+        source.writeString("b".repeat(SEGMENT_SIZE * 2))
         val out = ByteArrayOutputStream()
         source.copyTo(out, startIndex = 10L, endIndex = 10L + SEGMENT_SIZE * 3L)
         assertEquals(
@@ -46,49 +46,49 @@ class BufferTest {
         )
         assertEquals(
             "a".repeat(SEGMENT_SIZE * 2) + "b".repeat(SEGMENT_SIZE * 2),
-            source.readUtf8(SEGMENT_SIZE * 4L)
+            source.readString(SEGMENT_SIZE * 4L)
         )
     }
 
     @Test
     fun copyToSkippingSegments() {
         val source = Buffer()
-        source.writeUtf8("a".repeat(SEGMENT_SIZE * 2))
-        source.writeUtf8("b".repeat(SEGMENT_SIZE * 2))
+        source.writeString("a".repeat(SEGMENT_SIZE * 2))
+        source.writeString("b".repeat(SEGMENT_SIZE * 2))
         val out = ByteArrayOutputStream()
         source.copyTo(out, startIndex = SEGMENT_SIZE * 2 + 1L, endIndex = SEGMENT_SIZE * 2 + 4L)
         assertEquals("bbb", out.toString())
         assertEquals(
             "a".repeat(SEGMENT_SIZE * 2) + "b".repeat(SEGMENT_SIZE * 2),
-            source.readUtf8(SEGMENT_SIZE * 4L)
+            source.readString(SEGMENT_SIZE * 4L)
         )
     }
 
     @Test
     fun copyToStream() {
-        val buffer = Buffer().also { it.writeUtf8("hello, world!") }
+        val buffer = Buffer().also { it.writeString("hello, world!") }
         val out = ByteArrayOutputStream()
         buffer.copyTo(out)
         val outString = String(out.toByteArray(), UTF_8)
         assertEquals("hello, world!", outString)
-        assertEquals("hello, world!", buffer.readUtf8())
+        assertEquals("hello, world!", buffer.readString())
     }
 
     @Test
     fun writeToSpanningSegments() {
         val buffer = Buffer()
-        buffer.writeUtf8("a".repeat(SEGMENT_SIZE * 2))
-        buffer.writeUtf8("b".repeat(SEGMENT_SIZE * 2))
+        buffer.writeString("a".repeat(SEGMENT_SIZE * 2))
+        buffer.writeString("b".repeat(SEGMENT_SIZE * 2))
         val out = ByteArrayOutputStream()
         buffer.skip(10)
         buffer.readTo(out, SEGMENT_SIZE * 3L)
         assertEquals("a".repeat(SEGMENT_SIZE * 2 - 10) + "b".repeat(SEGMENT_SIZE + 10), out.toString())
-        assertEquals("b".repeat(SEGMENT_SIZE - 10), buffer.readUtf8(buffer.size))
+        assertEquals("b".repeat(SEGMENT_SIZE - 10), buffer.readString(buffer.size))
     }
 
     @Test
     fun writeToStream() {
-        val buffer = Buffer().also { it.writeUtf8("hello, world!") }
+        val buffer = Buffer().also { it.writeString("hello, world!") }
         val out = ByteArrayOutputStream()
         buffer.readTo(out)
         val outString = String(out.toByteArray(), UTF_8)
@@ -101,16 +101,16 @@ class BufferTest {
         val input: InputStream = ByteArrayInputStream("hello, world!".toByteArray(UTF_8))
         val buffer = Buffer()
         buffer.transferFrom(input)
-        val out = buffer.readUtf8()
+        val out = buffer.readString()
         assertEquals("hello, world!", out)
     }
 
     @Test
     fun readFromSpanningSegments() {
         val input: InputStream = ByteArrayInputStream("hello, world!".toByteArray(UTF_8))
-        val buffer = Buffer().also { it.writeUtf8("a".repeat(SEGMENT_SIZE - 10)) }
+        val buffer = Buffer().also { it.writeString("a".repeat(SEGMENT_SIZE - 10)) }
         buffer.transferFrom(input)
-        val out = buffer.readUtf8()
+        val out = buffer.readString()
         assertEquals("a".repeat(SEGMENT_SIZE - 10) + "hello, world!", out)
     }
 
@@ -119,7 +119,7 @@ class BufferTest {
         val input: InputStream = ByteArrayInputStream("hello, world!".toByteArray(UTF_8))
         val buffer = Buffer()
         buffer.write(input, 10)
-        val out = buffer.readUtf8()
+        val out = buffer.readString()
         assertEquals("hello, wor", out)
     }
 
@@ -149,7 +149,7 @@ class BufferTest {
     @Test
     fun bufferInputStreamByteByByte() {
         val source = Buffer()
-        source.writeUtf8("abc")
+        source.writeString("abc")
         val input: InputStream = source.asInputStream()
         assertEquals(3, input.available())
         assertEquals('a'.code, input.read())
@@ -162,7 +162,7 @@ class BufferTest {
     @Test
     fun bufferInputStreamBulkReads() {
         val source = Buffer()
-        source.writeUtf8("abc")
+        source.writeString("abc")
         val byteArray = ByteArray(4)
         Arrays.fill(byteArray, (-5).toByte())
         val input: InputStream = source.asInputStream()
@@ -176,78 +176,78 @@ class BufferTest {
     @Test
     fun copyToOutputStream() {
         val source = Buffer()
-        source.writeUtf8("party")
+        source.writeString("party")
 
         val target = Buffer()
         source.copyTo(target.asOutputStream())
-        assertEquals("party", target.readUtf8())
-        assertEquals("party", source.readUtf8())
+        assertEquals("party", target.readString())
+        assertEquals("party", source.readString())
     }
 
     @Test
     fun copyToOutputStreamWithStartIndex() {
         val source = Buffer()
-        source.writeUtf8("party")
+        source.writeString("party")
 
         val target = Buffer()
         source.copyTo(target.asOutputStream(), startIndex = 2)
-        assertEquals("rty", target.readUtf8())
-        assertEquals("party", source.readUtf8())
+        assertEquals("rty", target.readString())
+        assertEquals("party", source.readString())
     }
 
     @Test
     fun copyToOutputStreamWithEndIndex() {
         val source = Buffer()
-        source.writeUtf8("party")
+        source.writeString("party")
 
         val target = Buffer()
         source.copyTo(target.asOutputStream(), endIndex = 3)
-        assertEquals("par", target.readUtf8())
-        assertEquals("party", source.readUtf8())
+        assertEquals("par", target.readString())
+        assertEquals("party", source.readString())
     }
 
     @Test
     fun copyToOutputStreamWithIndices() {
         val source = Buffer()
-        source.writeUtf8("party")
+        source.writeString("party")
 
         val target = Buffer()
         source.copyTo(target.asOutputStream(), startIndex = 1, endIndex = 4)
-        assertEquals("art", target.readUtf8())
-        assertEquals("party", source.readUtf8())
+        assertEquals("art", target.readString())
+        assertEquals("party", source.readString())
     }
 
     @Test
     fun copyToOutputStreamWithEmptyRange() {
         val source = Buffer()
-        source.writeUtf8("hello")
+        source.writeString("hello")
 
         val target = Buffer()
         source.copyTo(target.asOutputStream(), startIndex = 1, endIndex = 1)
-        assertEquals("hello", source.readUtf8())
-        assertEquals("", target.readUtf8())
+        assertEquals("hello", source.readString())
+        assertEquals("", target.readString())
     }
 
     @Test
     fun readToOutputStream() {
         val source = Buffer()
-        source.writeUtf8("party")
+        source.writeString("party")
 
         val target = Buffer()
         source.readTo(target.asOutputStream())
-        assertEquals("party", target.readUtf8())
-        assertEquals("", source.readUtf8())
+        assertEquals("party", target.readString())
+        assertEquals("", source.readString())
     }
 
     @Test
     fun readToOutputStreamWithByteCount() {
         val source = Buffer()
-        source.writeUtf8("party")
+        source.writeString("party")
 
         val target = Buffer()
         source.readTo(target.asOutputStream(), byteCount = 3)
-        assertEquals("par", target.readUtf8())
-        assertEquals("ty", source.readUtf8())
+        assertEquals("par", target.readString())
+        assertEquals("ty", source.readString())
     }
 
     @Test
diff --git a/core/jvm/test/JvmPlatformTest.kt b/core/jvm/test/JvmPlatformTest.kt
index c07a62d9..ea6ee688 100644
--- a/core/jvm/test/JvmPlatformTest.kt
+++ b/core/jvm/test/JvmPlatformTest.kt
@@ -44,7 +44,7 @@ class JvmPlatformTest {
     fun outputStreamSink() {
         val baos = ByteArrayOutputStream()
         val sink = baos.asSink()
-        sink.write(Buffer().also { it.writeUtf8("a") }, 1L)
+        sink.write(Buffer().also { it.writeString("a") }, 1L)
         assertArrayEquals(baos.toByteArray(), byteArrayOf(0x61))
     }
 
@@ -52,7 +52,7 @@ class JvmPlatformTest {
     fun outputStreamSinkWriteZeroBytes() {
         val baos = ByteArrayOutputStream()
         val sink = baos.asSink()
-        sink.write(Buffer().also { it.writeUtf8("a") }, 0L)
+        sink.write(Buffer().also { it.writeString("a") }, 0L)
         assertEquals(0, baos.size())
     }
 
@@ -61,7 +61,7 @@ class JvmPlatformTest {
         val baos = ByteArrayOutputStream()
         val sink = baos.asSink()
         assertFailsWith<IllegalArgumentException> {
-            sink.write(Buffer().also { it.writeUtf8("a") }, -1)
+            sink.write(Buffer().also { it.writeString("a") }, -1)
         }
     }
 
@@ -69,10 +69,10 @@ class JvmPlatformTest {
     fun outputStreamSinkWritePartOfTheBuffer() {
         val baos = ByteArrayOutputStream()
         val sink = baos.asSink()
-        val buffer = Buffer().also { it.writeUtf8("hello") }
+        val buffer = Buffer().also { it.writeString("hello") }
         sink.write(buffer, 2)
         assertArrayEquals(baos.toByteArray(), byteArrayOf('h'.code.toByte(), 'e'.code.toByte()))
-        assertEquals("llo", buffer.readUtf8())
+        assertEquals("llo", buffer.readString())
     }
 
     @Test
@@ -81,7 +81,7 @@ class JvmPlatformTest {
         val source = bais.asSource()
         val buffer = Buffer()
         source.readAtMostTo(buffer, 1)
-        assertEquals(buffer.readUtf8(), "a")
+        assertEquals(buffer.readString(), "a")
     }
 
     @Test
@@ -104,7 +104,7 @@ class JvmPlatformTest {
     fun fileSink() {
         val file = File(tempDir, "test")
         file.outputStream().asSink().use { sink ->
-            sink.write(Buffer().also { it.writeUtf8("a") }, 1L)
+            sink.write(Buffer().also { it.writeString("a") }, 1L)
         }
         assertEquals(file.readText(), "a")
     }
@@ -114,7 +114,7 @@ class JvmPlatformTest {
         val file = File(tempDir, "test")
         file.writeText("a")
         FileOutputStream(file, true).asSink().use { sink ->
-            sink.write(Buffer().also { it.writeUtf8("b") }, 1L)
+            sink.write(Buffer().also { it.writeString("b") }, 1L)
         }
         assertEquals(file.readText(), "ab")
     }
@@ -127,14 +127,14 @@ class JvmPlatformTest {
         file.inputStream().asSource().use { source ->
             source.readAtMostTo(buffer, 1L)
         }
-        assertEquals(buffer.readUtf8(), "a")
+        assertEquals(buffer.readString(), "a")
     }
 
     @Test
     fun pathSink() {
         val file = File(tempDir, "test")
         file.toPath().outputStream().asSink().use { sink ->
-            sink.write(Buffer().also { it.writeUtf8("a") }, 1L)
+            sink.write(Buffer().also { it.writeString("a") }, 1L)
         }
         assertEquals(file.readText(), "a")
     }
@@ -144,7 +144,7 @@ class JvmPlatformTest {
         val file = File(tempDir, "test")
         file.writeText("a")
         file.toPath().outputStream(StandardOpenOption.APPEND).asSink().use { sink ->
-            sink.write(Buffer().also { it.writeUtf8("b") }, 1L)
+            sink.write(Buffer().also { it.writeString("b") }, 1L)
         }
         assertEquals(file.readText(), "ab")
     }
@@ -157,7 +157,7 @@ class JvmPlatformTest {
         file.toPath().inputStream().asSource().use { source ->
             source.readAtMostTo(buffer, 1L)
         }
-        assertEquals(buffer.readUtf8(), "a")
+        assertEquals(buffer.readString(), "a")
     }
 
     @Test
@@ -173,9 +173,9 @@ class JvmPlatformTest {
         }
 
         assertFailsWith<IOException> {
-            link.toPath().inputStream(LinkOption.NOFOLLOW_LINKS).asSource().use { it.buffered().readUtf8Line() }
+            link.toPath().inputStream(LinkOption.NOFOLLOW_LINKS).asSource().use { it.buffered().readLine() }
         }
-        assertNull(link.toPath().inputStream().asSource().use { it.buffered().readUtf8Line() })
+        assertNull(link.toPath().inputStream().asSource().use { it.buffered().readLine() })
     }
 
     @Test
@@ -185,7 +185,7 @@ class JvmPlatformTest {
             override fun getOutputStream() = baos
         }
         val sink = socket.outputStream.asSink()
-        sink.write(Buffer().also { it.writeUtf8("a") }, 1L)
+        sink.write(Buffer().also { it.writeString("a") }, 1L)
         assertArrayEquals(baos.toByteArray(), byteArrayOf(0x61))
     }
 
@@ -198,6 +198,6 @@ class JvmPlatformTest {
         val source = socket.inputStream.asSource()
         val buffer = Buffer()
         source.readAtMostTo(buffer, 1L)
-        assertEquals(buffer.readUtf8(), "a")
+        assertEquals(buffer.readString(), "a")
     }
 }
diff --git a/core/jvm/test/NioTest.kt b/core/jvm/test/NioTest.kt
index 541237da..96c59629 100644
--- a/core/jvm/test/NioTest.kt
+++ b/core/jvm/test/NioTest.kt
@@ -65,7 +65,7 @@ class NioTest {
         val fileChannel: FileChannel = FileChannel.open(file, StandardOpenOption.WRITE)
         testWritableByteChannel(false, fileChannel)
         val emitted: Source = file.inputStream().asSource().buffered()
-        assertEquals("defghijklmnopqrstuvw", emitted.readUtf8())
+        assertEquals("defghijklmnopqrstuvw", emitted.readString())
         emitted.close()
     }
 
@@ -73,7 +73,7 @@ class NioTest {
     fun writableChannelBuffer() {
         val buffer = Buffer()
         testWritableByteChannel(true, buffer.asByteChannel())
-        assertEquals("defghijklmnopqrstuvw", buffer.readUtf8())
+        assertEquals("defghijklmnopqrstuvw", buffer.readString())
     }
 
     @Test
@@ -81,14 +81,14 @@ class NioTest {
         val buffer = Buffer()
         val bufferedSink: Sink = buffer
         testWritableByteChannel(true, bufferedSink.asByteChannel())
-        assertEquals("defghijklmnopqrstuvw", buffer.readUtf8())
+        assertEquals("defghijklmnopqrstuvw", buffer.readString())
     }
 
     @Test
     fun readableChannelNioFile() {
         val file: File = Paths.get(temporaryFolder.toString(), "test").toFile()
         val initialData: Sink = file.outputStream().asSink().buffered()
-        initialData.writeUtf8("abcdefghijklmnopqrstuvwxyz")
+        initialData.writeString("abcdefghijklmnopqrstuvwxyz")
         initialData.close()
         val fileChannel: FileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ)
         testReadableByteChannel(false, fileChannel)
@@ -97,7 +97,7 @@ class NioTest {
     @Test
     fun readableChannelBuffer() {
         val buffer = Buffer()
-        buffer.writeUtf8("abcdefghijklmnopqrstuvwxyz")
+        buffer.writeString("abcdefghijklmnopqrstuvwxyz")
         testReadableByteChannel(true, buffer.asByteChannel())
     }
 
@@ -105,7 +105,7 @@ class NioTest {
     fun readableChannelBufferedSource() {
         val buffer = Buffer()
         val bufferedSource: Source = buffer
-        buffer.writeUtf8("abcdefghijklmnopqrstuvwxyz")
+        buffer.writeString("abcdefghijklmnopqrstuvwxyz")
         testReadableByteChannel(true, bufferedSource.asByteChannel())
     }