From 2dab5df7ca1c61feec6beb54933bfc206f568ecc Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Mon, 7 Aug 2023 08:10:09 +0200 Subject: [PATCH 1/4] Added extensions to integrate ByteString with Kotlin stdlib APIs Implemented integration standard library APIs with ByteStings: - Base64 - HexFormat Resolves #149 --- bytestring/common/src/Base64.kt | 240 ++++++++++++++++++ bytestring/common/src/Hex.kt | 63 +++++ .../common/test/ByteStringBase64Test.kt | 147 +++++++++++ bytestring/common/test/ByteStringHexTest.kt | 51 ++++ 4 files changed, 501 insertions(+) create mode 100644 bytestring/common/src/Base64.kt create mode 100644 bytestring/common/src/Hex.kt create mode 100644 bytestring/common/test/ByteStringBase64Test.kt create mode 100644 bytestring/common/test/ByteStringHexTest.kt diff --git a/bytestring/common/src/Base64.kt b/bytestring/common/src/Base64.kt new file mode 100644 index 000000000..bf4760dcd --- /dev/null +++ b/bytestring/common/src/Base64.kt @@ -0,0 +1,240 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalEncodingApi::class) + +package kotlinx.io.bytestring + +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.Base64.Default.encode +import kotlin.io.encoding.Base64.Default.encodeToByteArray +import kotlin.io.encoding.ExperimentalEncodingApi + +/** + * Encodes bytes from the specified [source] array or its subrange. + * Returns a [ByteArray] containing the resulting symbols. + * + * If the size of the [source] array or its subrange is not an integral multiple of 3, + * the result is padded with `'='` to an integral multiple of 4 symbols. + * + * Each resulting symbol occupies one byte in the returned byte array. + * + * Use [encode] to get the output in string form. + * + * @param source the array to encode bytes from. + * @param startIndex the beginning (inclusive) of the subrange to encode, 0 by default. + * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] array by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] array indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * + * @return a [ByteArray] with the resulting symbols. + */ +public fun Base64.encodeToByteArray(source: ByteString, startIndex: Int = 0, endIndex: Int = source.size): ByteArray { + return encodeToByteArray(source.getBackingArrayReference(), startIndex, endIndex) +} + +/** + * Encodes bytes from the specified [source] byte string or its subrange and writes resulting symbols into the [destination] array. + * Returns the number of symbols written. + * + * If the size of the [source] byte string or its subrange is not an integral multiple of 3, + * the result is padded with `'='` to an integral multiple of 4 symbols. + * + * @param source the byte string to encode bytes from. + * @param destination the array to write symbols into. + * @param destinationOffset the starting index in the [destination] array to write symbols to, 0 by default. + * @param startIndex the beginning (inclusive) of the subrange to encode, 0 by default. + * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] byte string by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] byte string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IndexOutOfBoundsException when the resulting symbols don't fit into the [destination] array starting at the specified [destinationOffset], + * or when that index is out of the [destination] array indices range. + * + * @return the number of symbols written into [destination] array. + */ +public fun Base64.encodeIntoByteArray( + source: ByteString, + destination: ByteArray, + destinationOffset: Int = 0, + startIndex: Int = 0, + endIndex: Int = source.size +): Int { + return encodeIntoByteArray(source.getBackingArrayReference(), destination, destinationOffset, startIndex, endIndex) +} + +/** + * Encodes bytes from the specified [source] byte string or its subrange. + * Returns a string with the resulting symbols. + * + * If the size of the [source] byte string or its subrange is not an integral multiple of 3, + * the result is padded with `'='` to an integral multiple of 4 symbols. + * + * Use [encodeToByteArray] to get the output in [ByteArray] form. + * + * @param source the byte string to encode bytes from. + * @param startIndex the beginning (inclusive) of the subrange to encode, 0 by default. + * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] byte string by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] array indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * + * @return a string with the resulting symbols. + */ +public fun Base64.encode( + source: ByteString, + startIndex: Int = 0, + endIndex: Int = source.size +): String { + return encode(source.getBackingArrayReference(), startIndex, endIndex) +} + +/** + * Encodes bytes from the specified [source] byte string or its subrange and appends resulting symbols to the [destination] appendable. + * Returns the destination appendable. + * + * If the size of the [source] byte string or its subrange is not an integral multiple of 3, + * the result is padded with `'='` to an integral multiple of 4 symbols. + * + * @param source the byte string to encode bytes from. + * @param destination the appendable to append symbols to. + * @param startIndex the beginning (inclusive) of the subrange to encode, 0 by default. + * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] byte string by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] array indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * + * @return the destination appendable. + */ +public fun Base64.encodeToAppendable( + source: ByteString, + destination: A, + startIndex: Int = 0, + endIndex: Int = source.size +): A { + return encodeToAppendable(source.getBackingArrayReference(), destination, startIndex, endIndex) +} + + +/** + * Decodes symbols from the specified [source] byte string or its subrange. + * Returns a [ByteArray] containing the resulting bytes. + * + * The symbols for decoding are not required to be padded. + * However, if there is a padding character present, the correct amount of padding character(s) must be present. + * The padding character `'='` is interpreted as the end of the encoded byte data. Subsequent symbols are prohibited. + * + * @param source the byte string to decode symbols from. + * @param startIndex the beginning (inclusive) of the subrange to decode, 0 by default. + * @param endIndex the end (exclusive) of the subrange to decode, size of the [source] byte string by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] byte string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException when the symbols for decoding are padded incorrectly or there are extra symbols after the padding. + * + * @return a [ByteArray] with the resulting bytes. + */ +public fun Base64.decode(source: ByteString, startIndex: Int = 0, endIndex: Int = source.size): ByteArray { + return decode(source.getBackingArrayReference(), startIndex, endIndex) +} + +/** + * Decodes symbols from the specified [source] char sequence or its substring. + * Returns a [ByteString] containing the resulting bytes. + * + * The symbols for decoding are not required to be padded. + * However, if there is a padding character present, the correct amount of padding character(s) must be present. + * The padding character `'='` is interpreted as the end of the encoded byte data. Subsequent symbols are prohibited. + * + * @param source the char sequence to decode symbols from. + * @param startIndex the beginning (inclusive) of the substring to decode, 0 by default. + * @param endIndex the end (exclusive) of the substring to decode, length of the [source] by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException when the symbols for decoding are padded incorrectly or there are extra symbols after the padding. + * + * @return a [ByteArray] with the resulting bytes. + */ +public fun Base64.decodeToByteString(source: CharSequence, startIndex: Int = 0, endIndex: Int = source.length): ByteString { + return ByteString.wrap(decode(source, startIndex, endIndex)) +} + +/** + * Decodes symbols from the specified [source] byte string or its subrange and writes resulting bytes into the [destination] array. + * Returns the number of bytes written. + * + * The symbols for decoding are not required to be padded. + * However, if there is a padding character present, the correct amount of padding character(s) must be present. + * The padding character `'='` is interpreted as the end of the encoded byte data. Subsequent symbols are prohibited. + * + * @param source the byte string to decode symbols from. + * @param destination the array to write bytes into. + * @param destinationOffset the starting index in the [destination] array to write bytes to, 0 by default. + * @param startIndex the beginning (inclusive) of the subrange to decode, 0 by default. + * @param endIndex the end (exclusive) of the subrange to decode, size of the [source] byte string by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] byte string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IndexOutOfBoundsException when the resulting bytes don'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 the symbols for decoding are padded incorrectly or there are extra symbols after the padding. + * + * @return the number of bytes written into [destination] array. + */ +public fun Base64.decodeIntoByteArray( + source: ByteString, + destination: ByteArray, + destinationOffset: Int = 0, + startIndex: Int = 0, + endIndex: Int = source.size +): Int { + return decodeIntoByteArray(source.getBackingArrayReference(), destination, destinationOffset, startIndex, endIndex) +} + +/** + * Decodes symbols from the specified [source] byte string or its subrange. + * Returns a [ByteString] containing the resulting bytes. + * + * The symbols for decoding are not required to be padded. + * However, if there is a padding character present, the correct amount of padding character(s) must be present. + * The padding character `'='` is interpreted as the end of the encoded byte data. Subsequent symbols are prohibited. + * + * @param source the byte string to decode symbols from. + * @param startIndex the beginning (inclusive) of the subrange to decode, 0 by default. + * @param endIndex the end (exclusive) of the subrange to decode, size of the [source] array by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] byte string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException when the symbols for decoding are padded incorrectly or there are extra symbols after the padding. + * + * @return a [ByteString] with the resulting bytes. + */ +public fun Base64.decodeToByteString(source: ByteArray, startIndex: Int = 0, endIndex: Int = source.size): ByteString { + return ByteString.wrap(decode(source, startIndex, endIndex)) +} + +/** + * Decodes symbols from the specified [source] byte string or its subrange. + * Returns a [ByteString] containing the resulting bytes. + * + * The symbols for decoding are not required to be padded. + * However, if there is a padding character present, the correct amount of padding character(s) must be present. + * The padding character `'='` is interpreted as the end of the encoded byte data. Subsequent symbols are prohibited. + * + * @param source the byte string to decode symbols from. + * @param startIndex the beginning (inclusive) of the subrange to decode, 0 by default. + * @param endIndex the end (exclusive) of the subrange to decode, size of the [source] byte string by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] byte string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException when the symbols for decoding are padded incorrectly or there are extra symbols after the padding. + * + * @return a [ByteString] with the resulting bytes. + */ +public fun Base64.decodeToByteString(source: ByteString, startIndex: Int = 0, endIndex: Int = source.size): ByteString { + return ByteString.wrap(decode(source.getBackingArrayReference(), startIndex, endIndex)) +} \ No newline at end of file diff --git a/bytestring/common/src/Hex.kt b/bytestring/common/src/Hex.kt new file mode 100644 index 000000000..21cad3db7 --- /dev/null +++ b/bytestring/common/src/Hex.kt @@ -0,0 +1,63 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalStdlibApi::class) + +package kotlinx.io.bytestring + +/** + * Formats bytes in this byte string using the specified [format]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.BytesHexFormat] affect formatting. + * + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + * + * @throws IllegalArgumentException if the result length is more than [String] maximum capacity. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +public fun ByteString.toHexString(format: HexFormat = HexFormat.Default): String { + return getBackingArrayReference().toHexString(0, getBackingArrayReference().size, format) +} + +/** + * Formats bytes in this byte string using the specified [HexFormat]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.BytesHexFormat] affect formatting. + * + * @param startIndex the beginning (inclusive) of the subrange to format, 0 by default. + * @param endIndex the end (exclusive) of the subrange to format, size of this byte string by default. + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this array indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException if the result length is more than [String] maximum capacity. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +public fun ByteString.toHexString( + startIndex: Int = 0, + endIndex: Int = size, + format: HexFormat = HexFormat.Default +): String { + return getBackingArrayReference().toHexString(startIndex, endIndex, format) +} + +/** + * Parses bytes from this string using the specified [HexFormat]. + * + * Note that only [HexFormat.BytesHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * Also, any of the char sequences CRLF, LF and CR is considered a valid line separator. + * + * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. + * + * @throws IllegalArgumentException if this string does not comply with the specified [format]. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +public fun String.hexToByteString(format: HexFormat = HexFormat.Default): ByteString { + return ByteString.wrap(hexToByteArray(format)) +} diff --git a/bytestring/common/test/ByteStringBase64Test.kt b/bytestring/common/test/ByteStringBase64Test.kt new file mode 100644 index 000000000..05798f038 --- /dev/null +++ b/bytestring/common/test/ByteStringBase64Test.kt @@ -0,0 +1,147 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalEncodingApi::class) + +package kotlinx.io.bytestring + +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.test.* + +class ByteStringBase64Test { + private fun bytes(vararg values: Int): ByteArray { + return ByteArray(values.size) { values[it].toByte() } + } + + + private val byteArray: ByteArray = bytes(0b0000_0100, 0b0010_0000, 0b1100_0100, 0b0001_0100, 0b0110_0001, 0b1100_1000) + private val byteString = ByteString.wrap(byteArray) + + private val encodedSymbols = "BCDEFGHI" + private val encodedBytes = encodedSymbols.encodeToByteArray() + private val encodedByteString = ByteString.wrap(encodedBytes) + + + @Test + fun testEncodeToByteArray() { + assertFailsWith { Base64.encodeToByteArray(byteString, startIndex = -1) } + assertFailsWith { Base64.encodeToByteArray(byteString, endIndex = byteString.size + 1) } + assertFailsWith { Base64.encodeToByteArray(byteString, startIndex = byteString.size + 1) } + assertFailsWith { Base64.encodeToByteArray(byteString, startIndex = 3, endIndex = 0) } + + assertTrue(Base64.encodeToByteArray(ByteString.EMPTY).isEmpty()) + assertContentEquals(encodedBytes, Base64.encodeToByteArray(byteString)) + + assertContentEquals(encodedSymbols.encodeToByteArray(0, 4), Base64.encodeToByteArray(byteString, endIndex = 3)) + assertContentEquals(encodedSymbols.encodeToByteArray(4), Base64.encodeToByteArray(byteString, startIndex = 3)) + } + + @Test + fun testEncodeIntoByteArray() { + val destination = ByteArray(encodedBytes.size) + + assertFailsWith { Base64.encodeIntoByteArray(byteString, destination, destinationOffset = -1) } + assertFailsWith { Base64.encodeIntoByteArray(byteString, destination, destinationOffset = destination.size + 1) } + assertFailsWith { Base64.encodeIntoByteArray(byteString, destination, destinationOffset = 1) } + + assertEquals(0, Base64.encodeIntoByteArray(ByteString.EMPTY, destination)) + assertEquals(encodedBytes.size, Base64.encodeIntoByteArray(byteString, destination)) + assertContentEquals(encodedBytes, destination.copyOf(encodedBytes.size)) + + var length = Base64.encodeIntoByteArray(byteString, destination, endIndex = 3) + assertContentEquals(encodedSymbols.encodeToByteArray(0, 4), destination.copyOf(length)) + length += Base64.encodeIntoByteArray(byteString, destination, destinationOffset = length, startIndex = 3) + assertContentEquals(encodedSymbols.encodeToByteArray(), destination) + } + + @Test + fun testEncode() { + assertFailsWith { Base64.encode(byteString, startIndex = -1) } + assertFailsWith { Base64.encode(byteString, endIndex = byteString.size + 1) } + assertFailsWith { Base64.encode(byteString, startIndex = byteString.size + 1) } + assertFailsWith { Base64.encode(byteString, startIndex = 3, endIndex = 0) } + + assertTrue(Base64.encode(ByteArray(0)).isEmpty()) + assertEquals(encodedSymbols, Base64.encode(byteString)) + assertEquals(encodedSymbols.substring(0, 4), Base64.encode(byteString, endIndex = 3)) + assertEquals(encodedSymbols.substring(4), Base64.encode(byteString, startIndex = 3)) + + val destination = StringBuilder() + Base64.encodeToAppendable(byteString, destination, endIndex = 3) + assertEquals(encodedSymbols.substring(0, 4), destination.toString()) + Base64.encodeToAppendable(byteString, destination, startIndex = 3) + assertEquals(encodedSymbols, destination.toString()) + } + + @Test + fun testDecode() { + assertFailsWith { Base64.decode(encodedByteString, startIndex = -1) } + assertFailsWith { Base64.decode(encodedByteString, endIndex = encodedByteString.size + 1) } + assertFailsWith { Base64.decode(encodedByteString, startIndex = encodedByteString.size + 1) } + assertFailsWith { Base64.decode(encodedByteString, startIndex = 4, endIndex = 0) } + + assertEquals(0, Base64.decode(ByteString.EMPTY).size) + assertContentEquals(byteArray, Base64.decode(encodedByteString)) + assertContentEquals(byteArray.copyOfRange(0, 3), Base64.decode(encodedByteString, endIndex = 4)) + assertContentEquals(byteArray.copyOfRange(3, byteString.size), Base64.decode(encodedByteString, startIndex = 4)) + } + + @Test + fun testDecodeIntoByteArray() { + val destination = ByteArray(6) + assertFailsWith { Base64.decodeIntoByteArray(encodedByteString, destination, destinationOffset = -1) } + assertFailsWith { Base64.decodeIntoByteArray(encodedByteString, destination, destinationOffset = destination.size + 1) } + assertFailsWith { Base64.decodeIntoByteArray(encodedByteString, destination, destinationOffset = 1) } + + assertTrue(destination.all { it == 0.toByte() }) + + assertEquals(0, Base64.decodeIntoByteArray(ByteString.EMPTY, destination)) + + var length = Base64.decodeIntoByteArray(encodedByteString, destination, endIndex = 4) + assertContentEquals(byteArray.copyOfRange(0, 3), destination.copyOf(length)) + length += Base64.decodeIntoByteArray(encodedByteString, destination, destinationOffset = length, startIndex = 4) + assertContentEquals(byteArray, destination) + } + + @Test + fun testDecodeToByteString() { + assertFailsWith { Base64.decodeToByteString(encodedSymbols, startIndex = -1) } + assertFailsWith { Base64.decodeToByteString(encodedSymbols, endIndex = encodedByteString.size + 1) } + assertFailsWith { Base64.decodeToByteString(encodedSymbols, startIndex = encodedByteString.size + 1) } + assertFailsWith { Base64.decodeToByteString(encodedSymbols, startIndex = 4, endIndex = 0) } + + assertEquals(0, Base64.decodeToByteString(ByteArray(0)).size) + assertEquals(byteString, Base64.decodeToByteString(encodedSymbols)) + assertEquals(ByteString.wrap(byteArray.copyOfRange(0, 3)), Base64.decodeToByteString(encodedSymbols, endIndex = 4)) + assertEquals(ByteString.wrap(byteArray.copyOfRange(3, byteString.size)), Base64.decodeToByteString(encodedSymbols, startIndex = 4)) + } + + @Test + fun testByteArrayDecodeToByteString() { + assertFailsWith { Base64.decodeToByteString(encodedBytes, startIndex = -1) } + assertFailsWith { Base64.decodeToByteString(encodedBytes, endIndex = encodedByteString.size + 1) } + assertFailsWith { Base64.decodeToByteString(encodedBytes, startIndex = encodedByteString.size + 1) } + assertFailsWith { Base64.decodeToByteString(encodedBytes, startIndex = 4, endIndex = 0) } + + assertEquals(0, Base64.decodeToByteString(ByteArray(0)).size) + assertEquals(byteString, Base64.decodeToByteString(encodedBytes)) + assertEquals(ByteString.wrap(byteArray.copyOfRange(0, 3)), Base64.decodeToByteString(encodedBytes, endIndex = 4)) + assertEquals(ByteString.wrap(byteArray.copyOfRange(3, byteString.size)), Base64.decodeToByteString(encodedBytes, startIndex = 4)) + } + + @Test + fun testByteStringDecodeToByteString() { + assertFailsWith { Base64.decodeToByteString(encodedByteString, startIndex = -1) } + assertFailsWith { Base64.decodeToByteString(encodedByteString, endIndex = encodedByteString.size + 1) } + assertFailsWith { Base64.decodeToByteString(encodedByteString, startIndex = encodedByteString.size + 1) } + assertFailsWith { Base64.decodeToByteString(encodedByteString, startIndex = 4, endIndex = 0) } + + assertEquals(0, Base64.decodeToByteString(ByteString.EMPTY).size) + assertEquals(byteString, Base64.decodeToByteString(encodedByteString)) + assertEquals(ByteString.wrap(byteArray.copyOfRange(0, 3)), Base64.decodeToByteString(encodedByteString, endIndex = 4)) + assertEquals(ByteString.wrap(byteArray.copyOfRange(3, byteString.size)), Base64.decodeToByteString(encodedByteString, startIndex = 4)) + } +} \ No newline at end of file diff --git a/bytestring/common/test/ByteStringHexTest.kt b/bytestring/common/test/ByteStringHexTest.kt new file mode 100644 index 000000000..cd764eb06 --- /dev/null +++ b/bytestring/common/test/ByteStringHexTest.kt @@ -0,0 +1,51 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalStdlibApi::class) + +package kotlinx.io.bytestring + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ByteStringHexTest { + private val byteString = ByteString.wrap(byteArrayOf(10, 11, 12, 13, 14, 15)) + + @Test + fun testEmpty() { + assertTrue(ByteString.EMPTY.toHexString().isEmpty()) + assertTrue("".hexToByteString().isEmpty()) + } + + @Test + fun testIndexes() { + assertEquals("0b0c", byteString.toHexString(1, 3)) + assertEquals("0c0d0e0f", byteString.toHexString(2)) + assertEquals("0a0b0c0d", byteString.toHexString(endIndex = 4)) + } + + @Test + fun testFormats() { + val format = HexFormat { + bytes { + byteSeparator = "|" + } + } + assertEquals("0a|0b|0c|0d|0e|0f", byteString.toHexString(format)) + assertEquals("0b|0c|0d", byteString.toHexString(1, 4, format)) + + assertEquals(byteString, "0a|0b|0c|0d|0e|0f".hexToByteString(format)) + } + + @Test + fun testDefault() { + assertEquals("0a0b0c0d0e0f", byteString.toHexString()) + assertEquals("0b0c0d", byteString.toHexString(1, 4)) + + assertEquals(byteString, "0a0b0c0d0e0f".hexToByteString()) + } + +} \ No newline at end of file From e61bbf95d9d02eb7d20ff07b36022b602e5e3e22 Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Mon, 7 Aug 2023 17:57:25 +0200 Subject: [PATCH 2/4] ~API --- bytestring/api/kotlinx-io-bytestring.api | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/bytestring/api/kotlinx-io-bytestring.api b/bytestring/api/kotlinx-io-bytestring.api index 8c054de27..b51c6fc9c 100644 --- a/bytestring/api/kotlinx-io-bytestring.api +++ b/bytestring/api/kotlinx-io-bytestring.api @@ -1,3 +1,24 @@ +public final class kotlinx/io/bytestring/Base64Kt { + public static final fun decode (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;II)[B + public static synthetic fun decode$default (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)[B + public static final fun decodeIntoByteArray (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;[BIII)I + public static synthetic fun decodeIntoByteArray$default (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;[BIIIILjava/lang/Object;)I + public static final fun decodeToByteString (Lkotlin/io/encoding/Base64;Ljava/lang/CharSequence;II)Lkotlinx/io/bytestring/ByteString; + public static final fun decodeToByteString (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;II)Lkotlinx/io/bytestring/ByteString; + public static final fun decodeToByteString (Lkotlin/io/encoding/Base64;[BII)Lkotlinx/io/bytestring/ByteString; + public static synthetic fun decodeToByteString$default (Lkotlin/io/encoding/Base64;Ljava/lang/CharSequence;IIILjava/lang/Object;)Lkotlinx/io/bytestring/ByteString; + public static synthetic fun decodeToByteString$default (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)Lkotlinx/io/bytestring/ByteString; + public static synthetic fun decodeToByteString$default (Lkotlin/io/encoding/Base64;[BIIILjava/lang/Object;)Lkotlinx/io/bytestring/ByteString; + public static final fun encode (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;II)Ljava/lang/String; + public static synthetic fun encode$default (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)Ljava/lang/String; + public static final fun encodeIntoByteArray (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;[BIII)I + public static synthetic fun encodeIntoByteArray$default (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;[BIIIILjava/lang/Object;)I + public static final fun encodeToAppendable (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;Ljava/lang/Appendable;II)Ljava/lang/Appendable; + public static synthetic fun encodeToAppendable$default (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;Ljava/lang/Appendable;IIILjava/lang/Object;)Ljava/lang/Appendable; + public static final fun encodeToByteArray (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;II)[B + public static synthetic fun encodeToByteArray$default (Lkotlin/io/encoding/Base64;Lkotlinx/io/bytestring/ByteString;IIILjava/lang/Object;)[B +} + public final class kotlinx/io/bytestring/ByteString : java/lang/Comparable { public static final field Companion Lkotlinx/io/bytestring/ByteString$Companion; public fun ([BII)V @@ -73,6 +94,15 @@ public final class kotlinx/io/bytestring/ByteStringKt { public static final fun startsWith (Lkotlinx/io/bytestring/ByteString;[B)Z } +public final class kotlinx/io/bytestring/HexKt { + public static final fun hexToByteString (Ljava/lang/String;Lkotlin/text/HexFormat;)Lkotlinx/io/bytestring/ByteString; + public static synthetic fun hexToByteString$default (Ljava/lang/String;Lkotlin/text/HexFormat;ILjava/lang/Object;)Lkotlinx/io/bytestring/ByteString; + public static final fun toHexString (Lkotlinx/io/bytestring/ByteString;IILkotlin/text/HexFormat;)Ljava/lang/String; + public static final fun toHexString (Lkotlinx/io/bytestring/ByteString;Lkotlin/text/HexFormat;)Ljava/lang/String; + public static synthetic fun toHexString$default (Lkotlinx/io/bytestring/ByteString;IILkotlin/text/HexFormat;ILjava/lang/Object;)Ljava/lang/String; + public static synthetic fun toHexString$default (Lkotlinx/io/bytestring/ByteString;Lkotlin/text/HexFormat;ILjava/lang/Object;)Ljava/lang/String; +} + public abstract interface annotation class kotlinx/io/bytestring/unsafe/UnsafeByteStringApi : java/lang/annotation/Annotation { } From bdf46b8f461ce35b9f528152fd115be4b9f2860c Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Tue, 8 Aug 2023 11:49:58 +0200 Subject: [PATCH 3/4] ~review fixes --- bytestring/common/src/Base64.kt | 18 +++++++++--------- bytestring/common/src/Hex.kt | 2 +- bytestring/common/test/ByteStringBase64Test.kt | 2 +- bytestring/common/test/ByteStringHexTest.kt | 12 +++++++++++- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/bytestring/common/src/Base64.kt b/bytestring/common/src/Base64.kt index bf4760dcd..9c8adb1d2 100644 --- a/bytestring/common/src/Base64.kt +++ b/bytestring/common/src/Base64.kt @@ -13,21 +13,21 @@ import kotlin.io.encoding.Base64.Default.encodeToByteArray import kotlin.io.encoding.ExperimentalEncodingApi /** - * Encodes bytes from the specified [source] array or its subrange. + * Encodes bytes from the specified [source] byte string or its subrange. * Returns a [ByteArray] containing the resulting symbols. * - * If the size of the [source] array or its subrange is not an integral multiple of 3, + * If the size of the [source] byte string or its subrange is not an integral multiple of 3, * the result is padded with `'='` to an integral multiple of 4 symbols. * * Each resulting symbol occupies one byte in the returned byte array. * * Use [encode] to get the output in string form. * - * @param source the array to encode bytes from. + * @param source the byte string to encode bytes from. * @param startIndex the beginning (inclusive) of the subrange to encode, 0 by default. - * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] array by default. + * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] byte string by default. * - * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] array indices. + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] byte string indices. * @throws IllegalArgumentException when `startIndex > endIndex`. * * @return a [ByteArray] with the resulting symbols. @@ -79,7 +79,7 @@ public fun Base64.encodeIntoByteArray( * @param startIndex the beginning (inclusive) of the subrange to encode, 0 by default. * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] byte string by default. * - * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] array indices. + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] byte string indices. * @throws IllegalArgumentException when `startIndex > endIndex`. * * @return a string with the resulting symbols. @@ -104,7 +104,7 @@ public fun Base64.encode( * @param startIndex the beginning (inclusive) of the subrange to encode, 0 by default. * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] byte string by default. * - * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] array indices. + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] byte string indices. * @throws IllegalArgumentException when `startIndex > endIndex`. * * @return the destination appendable. @@ -205,7 +205,7 @@ public fun Base64.decodeIntoByteArray( * * @param source the byte string to decode symbols from. * @param startIndex the beginning (inclusive) of the subrange to decode, 0 by default. - * @param endIndex the end (exclusive) of the subrange to decode, size of the [source] array by default. + * @param endIndex the end (exclusive) of the subrange to decode, size of the [source] byte string by default. * * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] byte string indices. * @throws IllegalArgumentException when `startIndex > endIndex`. @@ -237,4 +237,4 @@ public fun Base64.decodeToByteString(source: ByteArray, startIndex: Int = 0, end */ public fun Base64.decodeToByteString(source: ByteString, startIndex: Int = 0, endIndex: Int = source.size): ByteString { return ByteString.wrap(decode(source.getBackingArrayReference(), startIndex, endIndex)) -} \ No newline at end of file +} diff --git a/bytestring/common/src/Hex.kt b/bytestring/common/src/Hex.kt index 21cad3db7..78eb194f8 100644 --- a/bytestring/common/src/Hex.kt +++ b/bytestring/common/src/Hex.kt @@ -31,7 +31,7 @@ public fun ByteString.toHexString(format: HexFormat = HexFormat.Default): String * @param endIndex the end (exclusive) of the subrange to format, size of this byte string by default. * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. * - * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this array indices. + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this byte string indices. * @throws IllegalArgumentException when `startIndex > endIndex`. * @throws IllegalArgumentException if the result length is more than [String] maximum capacity. */ diff --git a/bytestring/common/test/ByteStringBase64Test.kt b/bytestring/common/test/ByteStringBase64Test.kt index 05798f038..482029096 100644 --- a/bytestring/common/test/ByteStringBase64Test.kt +++ b/bytestring/common/test/ByteStringBase64Test.kt @@ -144,4 +144,4 @@ class ByteStringBase64Test { assertEquals(ByteString.wrap(byteArray.copyOfRange(0, 3)), Base64.decodeToByteString(encodedByteString, endIndex = 4)) assertEquals(ByteString.wrap(byteArray.copyOfRange(3, byteString.size)), Base64.decodeToByteString(encodedByteString, startIndex = 4)) } -} \ No newline at end of file +} diff --git a/bytestring/common/test/ByteStringHexTest.kt b/bytestring/common/test/ByteStringHexTest.kt index cd764eb06..cd249a050 100644 --- a/bytestring/common/test/ByteStringHexTest.kt +++ b/bytestring/common/test/ByteStringHexTest.kt @@ -9,6 +9,7 @@ package kotlinx.io.bytestring import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFails import kotlin.test.assertTrue class ByteStringHexTest { @@ -25,6 +26,13 @@ class ByteStringHexTest { assertEquals("0b0c", byteString.toHexString(1, 3)) assertEquals("0c0d0e0f", byteString.toHexString(2)) assertEquals("0a0b0c0d", byteString.toHexString(endIndex = 4)) + + assertFails { byteString.toHexString(-1) } + assertFails { byteString.toHexString(endIndex = -1) } + assertFails { byteString.toHexString(3, 2) } + assertFails { byteString.toHexString(10) } + assertFails { byteString.toHexString(endIndex = 11) } + assertFails { byteString.toHexString(10, 11) } } @Test @@ -38,6 +46,7 @@ class ByteStringHexTest { assertEquals("0b|0c|0d", byteString.toHexString(1, 4, format)) assertEquals(byteString, "0a|0b|0c|0d|0e|0f".hexToByteString(format)) + assertFails { "0a0b0c0d0e0f".hexToByteString(format) } } @Test @@ -46,6 +55,7 @@ class ByteStringHexTest { assertEquals("0b0c0d", byteString.toHexString(1, 4)) assertEquals(byteString, "0a0b0c0d0e0f".hexToByteString()) + assertFails { "0a|0b|0c|0d|0e|0f".hexToByteString() } } -} \ No newline at end of file +} From 631f1dcb1df07cd1340dcab24ccd00d6ea0104db Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Tue, 8 Aug 2023 13:07:33 +0200 Subject: [PATCH 4/4] ~review fixes --- bytestring/common/src/Base64.kt | 11 +++++++++-- bytestring/common/src/Hex.kt | 5 ----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/bytestring/common/src/Base64.kt b/bytestring/common/src/Base64.kt index 9c8adb1d2..f21ebc4e1 100644 --- a/bytestring/common/src/Base64.kt +++ b/bytestring/common/src/Base64.kt @@ -3,8 +3,6 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. */ -@file:OptIn(ExperimentalEncodingApi::class) - package kotlinx.io.bytestring import kotlin.io.encoding.Base64 @@ -32,6 +30,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi * * @return a [ByteArray] with the resulting symbols. */ +@ExperimentalEncodingApi public fun Base64.encodeToByteArray(source: ByteString, startIndex: Int = 0, endIndex: Int = source.size): ByteArray { return encodeToByteArray(source.getBackingArrayReference(), startIndex, endIndex) } @@ -56,6 +55,7 @@ public fun Base64.encodeToByteArray(source: ByteString, startIndex: Int = 0, end * * @return the number of symbols written into [destination] array. */ +@ExperimentalEncodingApi public fun Base64.encodeIntoByteArray( source: ByteString, destination: ByteArray, @@ -84,6 +84,7 @@ public fun Base64.encodeIntoByteArray( * * @return a string with the resulting symbols. */ +@ExperimentalEncodingApi public fun Base64.encode( source: ByteString, startIndex: Int = 0, @@ -109,6 +110,7 @@ public fun Base64.encode( * * @return the destination appendable. */ +@ExperimentalEncodingApi public fun Base64.encodeToAppendable( source: ByteString, destination: A, @@ -137,6 +139,7 @@ public fun Base64.encodeToAppendable( * * @return a [ByteArray] with the resulting bytes. */ +@ExperimentalEncodingApi public fun Base64.decode(source: ByteString, startIndex: Int = 0, endIndex: Int = source.size): ByteArray { return decode(source.getBackingArrayReference(), startIndex, endIndex) } @@ -159,6 +162,7 @@ public fun Base64.decode(source: ByteString, startIndex: Int = 0, endIndex: Int * * @return a [ByteArray] with the resulting bytes. */ +@ExperimentalEncodingApi public fun Base64.decodeToByteString(source: CharSequence, startIndex: Int = 0, endIndex: Int = source.length): ByteString { return ByteString.wrap(decode(source, startIndex, endIndex)) } @@ -185,6 +189,7 @@ public fun Base64.decodeToByteString(source: CharSequence, startIndex: Int = 0, * * @return the number of bytes written into [destination] array. */ +@ExperimentalEncodingApi public fun Base64.decodeIntoByteArray( source: ByteString, destination: ByteArray, @@ -213,6 +218,7 @@ public fun Base64.decodeIntoByteArray( * * @return a [ByteString] with the resulting bytes. */ +@ExperimentalEncodingApi public fun Base64.decodeToByteString(source: ByteArray, startIndex: Int = 0, endIndex: Int = source.size): ByteString { return ByteString.wrap(decode(source, startIndex, endIndex)) } @@ -235,6 +241,7 @@ public fun Base64.decodeToByteString(source: ByteArray, startIndex: Int = 0, end * * @return a [ByteString] with the resulting bytes. */ +@ExperimentalEncodingApi public fun Base64.decodeToByteString(source: ByteString, startIndex: Int = 0, endIndex: Int = source.size): ByteString { return ByteString.wrap(decode(source.getBackingArrayReference(), startIndex, endIndex)) } diff --git a/bytestring/common/src/Hex.kt b/bytestring/common/src/Hex.kt index 78eb194f8..4ecb46b6e 100644 --- a/bytestring/common/src/Hex.kt +++ b/bytestring/common/src/Hex.kt @@ -3,8 +3,6 @@ * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. */ -@file:OptIn(ExperimentalStdlibApi::class) - package kotlinx.io.bytestring /** @@ -17,7 +15,6 @@ package kotlinx.io.bytestring * @throws IllegalArgumentException if the result length is more than [String] maximum capacity. */ @ExperimentalStdlibApi -@SinceKotlin("1.9") public fun ByteString.toHexString(format: HexFormat = HexFormat.Default): String { return getBackingArrayReference().toHexString(0, getBackingArrayReference().size, format) } @@ -36,7 +33,6 @@ public fun ByteString.toHexString(format: HexFormat = HexFormat.Default): String * @throws IllegalArgumentException if the result length is more than [String] maximum capacity. */ @ExperimentalStdlibApi -@SinceKotlin("1.9") public fun ByteString.toHexString( startIndex: Int = 0, endIndex: Int = size, @@ -57,7 +53,6 @@ public fun ByteString.toHexString( * @throws IllegalArgumentException if this string does not comply with the specified [format]. */ @ExperimentalStdlibApi -@SinceKotlin("1.9") public fun String.hexToByteString(format: HexFormat = HexFormat.Default): ByteString { return ByteString.wrap(hexToByteArray(format)) }