diff --git a/benchmarks/src/commonMain/kotlin/BufferOps.kt b/benchmarks/src/commonMain/kotlin/BufferOps.kt index fc1d6d58b..cffcc673b 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/bytestring/Module.md b/bytestring/Module.md new file mode 100644 index 000000000..9a3193763 --- /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 000000000..8c054de27 --- /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 ([BII)V + public synthetic fun ([BIIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun ([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 ()V + public fun (I)V + public synthetic fun (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 000000000..f81792b08 --- /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().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 000000000..155ee4fa8 --- /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 { + /** + * 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 000000000..3d62964e8 --- /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 000000000..41ece7620 --- /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 000000000..fdc89e3b2 --- /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 000000000..2fbb05408 --- /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 { builder.append(array, 2, 0) } + assertFailsWith { builder.append(array, -1, 2) } + assertFailsWith { builder.append(array, 0, 1000) } + assertFailsWith { 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 000000000..6048d7419 --- /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 { str[-1] } + assertFailsWith { 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 { str.toByteArray(-1, 1) } + assertFailsWith { str.toByteArray(1, 4) } + assertFailsWith { str.toByteArray(-1, 4) } + assertFailsWith { 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 { str.copyInto(dest, 0, startIndex = 1, endIndex = 0) } + assertFailsWith { str.copyInto(dest, 9) } + assertFailsWith { str.copyInto(dest, -1) } + assertFailsWith { str.copyInto(dest, 0, startIndex = -1) } + assertFailsWith { 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 { str.substring(2, 1) } + assertFailsWith { str.substring(-1) } + assertFailsWith { str.substring(0, 10) } + assertFailsWith { 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 000000000..555e65f86 --- /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 000000000..37f9b86ae --- /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/api/kotlinx-io-core.api b/core/api/kotlinx-io-core.api index 75e8d9085..38cb45ff8 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 850c033e9..02cb605bb 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 975473abd..12724a16c 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 000000000..07fd950d5 --- /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/ByteStringExt.kt b/core/common/src/ByteStringExt.kt new file mode 100644 index 000000000..b5f0f6034 --- /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/src/SinkExt.kt b/core/common/src/SinkExt.kt index 0527b2e17..f0d5385c9 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 59f20ca8a..811ce44c9 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 8e67c0b88..98f33e019 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/common/test/AbstractSinkTest.kt b/core/common/test/AbstractSinkTest.kt index 38882154b..22e048d60 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 { 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 { 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 { sink.writeUtf8("hello", startIndex = -1) } - assertFailsWith { sink.writeUtf8("hello", startIndex = 0, endIndex = 6) } - assertFailsWith { sink.writeUtf8("hello", startIndex = 6) } + assertFailsWith { sink.writeString("hello", startIndex = -1) } + assertFailsWith { sink.writeString("hello", startIndex = 0, endIndex = 6) } + assertFailsWith { 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 e89f418b5..c27f665f6 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 { @@ -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 { 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 { - 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 { 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("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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { source.readDecimalLong() @@ -1073,22 +1076,22 @@ abstract class AbstractBufferedSourceTest internal constructor( @Test fun longDecimalLoneDashThrows() { - sink.writeUtf8("-") + sink.writeString("-") sink.emit() assertFailsWith { source.readDecimalLong() } - assertEquals("-", source.readUtf8()) + assertEquals("-", source.readString()) } @Test fun longDecimalDashFollowedByNonDigitThrows() { - sink.writeUtf8("- ") + sink.writeString("- ") sink.emit() assertFailsWith { 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 { - 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 { source.readUtf8LineStrict() } + assertEquals("first line", source.readLineStrict()) + assertEquals("second line\n", source.readString()) + assertFailsWith { 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 { source.readUtf8LineStrict() } - assertEquals("There is no newline!", source.readUtf8()) + assertFailsWith { 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 { source.readUtf8LineStrict(3) } - assertEquals("line", source.readUtf8LineStrict(4)) + assertFailsWith { source.readLineStrict(3) } + assertEquals("line", source.readLineStrict(4)) assertTrue(source.exhausted()) - sink.writeUtf8("line\r\n") + sink.writeString("line\r\n") sink.flush() - assertFailsWith { source.readUtf8LineStrict(3) } - assertEquals("line", source.readUtf8LineStrict(4)) + assertFailsWith { 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 { 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 { + 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 abd257895..d450557a2 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 { - 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 79d6734d8..2cf8faa20 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 2ee1a6452..f6b454e97 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 9f056b28e..b345516f2 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 e526dcb9a..d040f8a0f 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 573360912..f47a12adf 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/src/SinkExtJvm.kt b/core/jvm/src/SinkExtJvm.kt index d74bad456..494b5c563 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/core/jvm/test/AbstractSinkTestJVM.kt b/core/jvm/test/AbstractSinkTestJVM.kt index d383c141d..c143c151c 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 5a3f31ef3..dec531d66 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 { @@ -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 { 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 bab8a2cdf..693cf67ca 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 c07a62d99..ea6ee688e 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 { - 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 { - 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 541237da6..96c59629d 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()) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9d18495a5..912ac632c 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")