From 63a5a746132e5dc37672cec585d5b4dcca0c6577 Mon Sep 17 00:00:00 2001 From: Abduqodiri Qurbonzoda Date: Wed, 26 Apr 2023 09:54:25 +0000 Subject: [PATCH] Introduce HexFormat for formatting and parsing hexadecimals #KT-57762 Merge-request: KT-MR-9460 Merged-by: Abduqodiri Qurbonzoda --- compiler/testData/codegen/box/size/add.kt | 2 +- .../codegen/box/size/helloWorldDOM.kt | 2 +- compiler/testData/codegen/box/size/ok.kt | 2 +- .../nativeClassAsReifiedTypeArgument.kt | 2 +- .../testData/box/reflection/classJsName.kt | 2 +- .../testData/box/reflection/primitives-11.kt | 2 +- libraries/stdlib/api/js-v1/kotlin.text.kt | 187 ++++++ libraries/stdlib/api/js/kotlin.text.kt | 187 ++++++ .../stdlib/src/kotlin/text/HexExtensions.kt | 593 ++++++++++++++++++ libraries/stdlib/src/kotlin/text/HexFormat.kt | 411 ++++++++++++ .../stdlib/src/kotlin/text/UHexExtensions.kt | 287 +++++++++ .../stdlib/test/text/BytesHexFormatTest.kt | 569 +++++++++++++++++ .../stdlib/test/text/NumberHexFormatTest.kt | 211 +++++++ .../kotlin-stdlib-runtime-merged.txt | 88 +++ 14 files changed, 2539 insertions(+), 6 deletions(-) create mode 100644 libraries/stdlib/src/kotlin/text/HexExtensions.kt create mode 100644 libraries/stdlib/src/kotlin/text/HexFormat.kt create mode 100644 libraries/stdlib/src/kotlin/text/UHexExtensions.kt create mode 100644 libraries/stdlib/test/text/BytesHexFormatTest.kt create mode 100644 libraries/stdlib/test/text/NumberHexFormatTest.kt diff --git a/compiler/testData/codegen/box/size/add.kt b/compiler/testData/codegen/box/size/add.kt index 76b96555f5e..1b433b02883 100644 --- a/compiler/testData/codegen/box/size/add.kt +++ b/compiler/testData/codegen/box/size/add.kt @@ -1,6 +1,6 @@ // TARGET_BACKEND: WASM -// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 12_295 +// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 12_463 // WASM_DCE_EXPECTED_OUTPUT_SIZE: mjs 5_579 // FILE: test.kt diff --git a/compiler/testData/codegen/box/size/helloWorldDOM.kt b/compiler/testData/codegen/box/size/helloWorldDOM.kt index edb51f7eee9..1bc092a0c97 100644 --- a/compiler/testData/codegen/box/size/helloWorldDOM.kt +++ b/compiler/testData/codegen/box/size/helloWorldDOM.kt @@ -1,6 +1,6 @@ // TARGET_BACKEND: WASM -// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 13_660 +// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 13_828 // WASM_DCE_EXPECTED_OUTPUT_SIZE: mjs 6_134 // FILE: test.kt diff --git a/compiler/testData/codegen/box/size/ok.kt b/compiler/testData/codegen/box/size/ok.kt index 39bead109dd..ea17b788f36 100644 --- a/compiler/testData/codegen/box/size/ok.kt +++ b/compiler/testData/codegen/box/size/ok.kt @@ -1,6 +1,6 @@ // TARGET_BACKEND: WASM -// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 12_337 +// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 12_505 // WASM_DCE_EXPECTED_OUTPUT_SIZE: mjs 5_451 fun box() = "OK" \ No newline at end of file diff --git a/js/js.translator/testData/box/native/nativeClassAsReifiedTypeArgument.kt b/js/js.translator/testData/box/native/nativeClassAsReifiedTypeArgument.kt index b035928e230..ba80be41629 100644 --- a/js/js.translator/testData/box/native/nativeClassAsReifiedTypeArgument.kt +++ b/js/js.translator/testData/box/native/nativeClassAsReifiedTypeArgument.kt @@ -1,6 +1,6 @@ // DONT_TARGET_EXACT_BACKEND: WASM // WASM_MUTE_REASON: UNSUPPORTED_JS_INTEROP -// EXPECTED_REACHABLE_NODES: 1283 +// EXPECTED_REACHABLE_NODES: 1412 //FILE: nativeClassAsReifiedTypeArgument.kt var global = "" diff --git a/js/js.translator/testData/box/reflection/classJsName.kt b/js/js.translator/testData/box/reflection/classJsName.kt index 47a490fa32c..7de7d4a0ade 100644 --- a/js/js.translator/testData/box/reflection/classJsName.kt +++ b/js/js.translator/testData/box/reflection/classJsName.kt @@ -1,4 +1,4 @@ -// EXPECTED_REACHABLE_NODES: 1282 +// EXPECTED_REACHABLE_NODES: 1419 fun Throwable.className() = this::class.simpleName!! diff --git a/js/js.translator/testData/box/reflection/primitives-11.kt b/js/js.translator/testData/box/reflection/primitives-11.kt index 908084ed128..8a8acc55827 100644 --- a/js/js.translator/testData/box/reflection/primitives-11.kt +++ b/js/js.translator/testData/box/reflection/primitives-11.kt @@ -1,4 +1,4 @@ -// EXPECTED_REACHABLE_NODES: 1290 +// EXPECTED_REACHABLE_NODES: 1432 // !LANGUAGE: -ProhibitGenericArrayClassLiteral import kotlin.reflect.KClass diff --git a/libraries/stdlib/api/js-v1/kotlin.text.kt b/libraries/stdlib/api/js-v1/kotlin.text.kt index 795c75f3bbd..15952c328ae 100644 --- a/libraries/stdlib/api/js-v1/kotlin.text.kt +++ b/libraries/stdlib/api/js-v1/kotlin.text.kt @@ -8,6 +8,11 @@ public val kotlin.CharSequence.indices: kotlin.ranges.IntRange { get; } public val kotlin.CharSequence.lastIndex: kotlin.Int { get; } +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun HexFormat(builderAction: kotlin.text.HexFormat.Builder.() -> kotlin.Unit): kotlin.text.HexFormat + @kotlin.SinceKotlin(version = "1.2") @kotlin.Deprecated(message = "Use CharArray.concatToString() instead", replaceWith = kotlin.ReplaceWith(expression = "chars.concatToString()", imports = {})) @kotlin.DeprecatedSinceKotlin(errorSince = "1.5", warningSince = "1.4") @@ -355,6 +360,52 @@ public inline fun kotlin.CharSequence.groupingBy(crossinline keySelector: (k public fun kotlin.CharSequence.hasSurrogatePairAt(index: kotlin.Int): kotlin.Boolean +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToByte(format: kotlin.text.HexFormat = ...): kotlin.Byte + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToByteArray(format: kotlin.text.HexFormat = ...): kotlin.ByteArray + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToInt(format: kotlin.text.HexFormat = ...): kotlin.Int + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToLong(format: kotlin.text.HexFormat = ...): kotlin.Long + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToShort(format: kotlin.text.HexFormat = ...): kotlin.Short + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToUByte(format: kotlin.text.HexFormat = ...): kotlin.UByte + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToUByteArray(format: kotlin.text.HexFormat = ...): kotlin.UByteArray + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToUInt(format: kotlin.text.HexFormat = ...): kotlin.UInt + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToULong(format: kotlin.text.HexFormat = ...): kotlin.ULong + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToUShort(format: kotlin.text.HexFormat = ...): kotlin.UShort + @kotlin.SinceKotlin(version = "1.3") @kotlin.internal.InlineOnly public inline fun C.ifBlank(defaultValue: () -> R): R where C : R @@ -1004,6 +1055,62 @@ public inline fun kotlin.String.toFloatOrNull(): kotlin.Float? public fun kotlin.CharSequence.toHashSet(): kotlin.collections.HashSet +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.Byte.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.ByteArray.toHexString(startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ..., format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.ByteArray.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.Int.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.Long.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.Short.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.UByte.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun kotlin.UByteArray.toHexString(startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ..., format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun kotlin.UByteArray.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.UInt.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.ULong.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.UShort.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + public fun kotlin.String.toInt(): kotlin.Int public fun kotlin.String.toInt(radix: kotlin.Int): kotlin.Int @@ -1332,6 +1439,86 @@ public open class CharacterCodingException : kotlin.Exception { public constructor CharacterCodingException(message: kotlin.String?) } +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public final class HexFormat { + public final val bytes: kotlin.text.HexFormat.BytesHexFormat { get; } + + public final val number: kotlin.text.HexFormat.NumberHexFormat { get; } + + public final val upperCase: kotlin.Boolean { get; } + + public open override fun toString(): kotlin.String + + public final class Builder { + public final val bytes: kotlin.text.HexFormat.BytesHexFormat.Builder { get; } + + public final val number: kotlin.text.HexFormat.NumberHexFormat.Builder { get; } + + public final var upperCase: kotlin.Boolean { get; set; } + + @kotlin.internal.InlineOnly + public final inline fun bytes(builderAction: kotlin.text.HexFormat.BytesHexFormat.Builder.() -> kotlin.Unit): kotlin.Unit + + @kotlin.internal.InlineOnly + public final inline fun number(builderAction: kotlin.text.HexFormat.NumberHexFormat.Builder.() -> kotlin.Unit): kotlin.Unit + } + + public final class BytesHexFormat { + public final val bytePrefix: kotlin.String { get; } + + public final val byteSeparator: kotlin.String { get; } + + public final val byteSuffix: kotlin.String { get; } + + public final val bytesPerGroup: kotlin.Int { get; } + + public final val bytesPerLine: kotlin.Int { get; } + + public final val groupSeparator: kotlin.String { get; } + + public open override fun toString(): kotlin.String + + public final class Builder { + public final var bytePrefix: kotlin.String { get; set; } + + public final var byteSeparator: kotlin.String { get; set; } + + public final var byteSuffix: kotlin.String { get; set; } + + public final var bytesPerGroup: kotlin.Int { get; set; } + + public final var bytesPerLine: kotlin.Int { get; set; } + + public final var groupSeparator: kotlin.String { get; set; } + } + } + + public companion object of HexFormat { + public final val Default: kotlin.text.HexFormat { get; } + + public final val UpperCase: kotlin.text.HexFormat { get; } + } + + public final class NumberHexFormat { + public final val prefix: kotlin.String { get; } + + public final val removeLeadingZeros: kotlin.Boolean { get; } + + public final val suffix: kotlin.String { get; } + + public open override fun toString(): kotlin.String + + public final class Builder { + public final var prefix: kotlin.String { get; set; } + + public final var removeLeadingZeros: kotlin.Boolean { get; set; } + + public final var suffix: kotlin.String { get; set; } + } + } +} + public final data class MatchGroup { public constructor MatchGroup(value: kotlin.String) diff --git a/libraries/stdlib/api/js/kotlin.text.kt b/libraries/stdlib/api/js/kotlin.text.kt index 795c75f3bbd..15952c328ae 100644 --- a/libraries/stdlib/api/js/kotlin.text.kt +++ b/libraries/stdlib/api/js/kotlin.text.kt @@ -8,6 +8,11 @@ public val kotlin.CharSequence.indices: kotlin.ranges.IntRange { get; } public val kotlin.CharSequence.lastIndex: kotlin.Int { get; } +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun HexFormat(builderAction: kotlin.text.HexFormat.Builder.() -> kotlin.Unit): kotlin.text.HexFormat + @kotlin.SinceKotlin(version = "1.2") @kotlin.Deprecated(message = "Use CharArray.concatToString() instead", replaceWith = kotlin.ReplaceWith(expression = "chars.concatToString()", imports = {})) @kotlin.DeprecatedSinceKotlin(errorSince = "1.5", warningSince = "1.4") @@ -355,6 +360,52 @@ public inline fun kotlin.CharSequence.groupingBy(crossinline keySelector: (k public fun kotlin.CharSequence.hasSurrogatePairAt(index: kotlin.Int): kotlin.Boolean +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToByte(format: kotlin.text.HexFormat = ...): kotlin.Byte + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToByteArray(format: kotlin.text.HexFormat = ...): kotlin.ByteArray + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToInt(format: kotlin.text.HexFormat = ...): kotlin.Int + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToLong(format: kotlin.text.HexFormat = ...): kotlin.Long + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.String.hexToShort(format: kotlin.text.HexFormat = ...): kotlin.Short + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToUByte(format: kotlin.text.HexFormat = ...): kotlin.UByte + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToUByteArray(format: kotlin.text.HexFormat = ...): kotlin.UByteArray + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToUInt(format: kotlin.text.HexFormat = ...): kotlin.UInt + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToULong(format: kotlin.text.HexFormat = ...): kotlin.ULong + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.String.hexToUShort(format: kotlin.text.HexFormat = ...): kotlin.UShort + @kotlin.SinceKotlin(version = "1.3") @kotlin.internal.InlineOnly public inline fun C.ifBlank(defaultValue: () -> R): R where C : R @@ -1004,6 +1055,62 @@ public inline fun kotlin.String.toFloatOrNull(): kotlin.Float? public fun kotlin.CharSequence.toHashSet(): kotlin.collections.HashSet +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.Byte.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.ByteArray.toHexString(startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ..., format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.ByteArray.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.Int.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.Long.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public fun kotlin.Short.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.UByte.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun kotlin.UByteArray.toHexString(startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ..., format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun kotlin.UByteArray.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.UInt.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.ULong.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +@kotlin.internal.InlineOnly +public inline fun kotlin.UShort.toHexString(format: kotlin.text.HexFormat = ...): kotlin.String + public fun kotlin.String.toInt(): kotlin.Int public fun kotlin.String.toInt(radix: kotlin.Int): kotlin.Int @@ -1332,6 +1439,86 @@ public open class CharacterCodingException : kotlin.Exception { public constructor CharacterCodingException(message: kotlin.String?) } +@kotlin.ExperimentalStdlibApi +@kotlin.SinceKotlin(version = "1.9") +public final class HexFormat { + public final val bytes: kotlin.text.HexFormat.BytesHexFormat { get; } + + public final val number: kotlin.text.HexFormat.NumberHexFormat { get; } + + public final val upperCase: kotlin.Boolean { get; } + + public open override fun toString(): kotlin.String + + public final class Builder { + public final val bytes: kotlin.text.HexFormat.BytesHexFormat.Builder { get; } + + public final val number: kotlin.text.HexFormat.NumberHexFormat.Builder { get; } + + public final var upperCase: kotlin.Boolean { get; set; } + + @kotlin.internal.InlineOnly + public final inline fun bytes(builderAction: kotlin.text.HexFormat.BytesHexFormat.Builder.() -> kotlin.Unit): kotlin.Unit + + @kotlin.internal.InlineOnly + public final inline fun number(builderAction: kotlin.text.HexFormat.NumberHexFormat.Builder.() -> kotlin.Unit): kotlin.Unit + } + + public final class BytesHexFormat { + public final val bytePrefix: kotlin.String { get; } + + public final val byteSeparator: kotlin.String { get; } + + public final val byteSuffix: kotlin.String { get; } + + public final val bytesPerGroup: kotlin.Int { get; } + + public final val bytesPerLine: kotlin.Int { get; } + + public final val groupSeparator: kotlin.String { get; } + + public open override fun toString(): kotlin.String + + public final class Builder { + public final var bytePrefix: kotlin.String { get; set; } + + public final var byteSeparator: kotlin.String { get; set; } + + public final var byteSuffix: kotlin.String { get; set; } + + public final var bytesPerGroup: kotlin.Int { get; set; } + + public final var bytesPerLine: kotlin.Int { get; set; } + + public final var groupSeparator: kotlin.String { get; set; } + } + } + + public companion object of HexFormat { + public final val Default: kotlin.text.HexFormat { get; } + + public final val UpperCase: kotlin.text.HexFormat { get; } + } + + public final class NumberHexFormat { + public final val prefix: kotlin.String { get; } + + public final val removeLeadingZeros: kotlin.Boolean { get; } + + public final val suffix: kotlin.String { get; } + + public open override fun toString(): kotlin.String + + public final class Builder { + public final var prefix: kotlin.String { get; set; } + + public final var removeLeadingZeros: kotlin.Boolean { get; set; } + + public final var suffix: kotlin.String { get; set; } + } + } +} + public final data class MatchGroup { public constructor MatchGroup(value: kotlin.String) diff --git a/libraries/stdlib/src/kotlin/text/HexExtensions.kt b/libraries/stdlib/src/kotlin/text/HexExtensions.kt new file mode 100644 index 00000000000..253a6c7261b --- /dev/null +++ b/libraries/stdlib/src/kotlin/text/HexExtensions.kt @@ -0,0 +1,593 @@ +/* + * 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/LICENSE.txt file. + */ + +package kotlin.text + +import kotlin.native.concurrent.SharedImmutable + +private const val LOWER_CASE_HEX_DIGITS = "0123456789abcdef" +private const val UPPER_CASE_HEX_DIGITS = "0123456789ABCDEF" + +// case-insensitive parsing +@SharedImmutable +private val HEX_DIGITS_TO_DECIMAL = IntArray(128) { -1 }.apply { + LOWER_CASE_HEX_DIGITS.forEachIndexed { index, char -> this[char.code] = index } + UPPER_CASE_HEX_DIGITS.forEachIndexed { index, char -> this[char.code] = index } +} + +// -------------------------- format and parse ByteArray -------------------------- + +/** + * Formats bytes in this array 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 ByteArray.toHexString(format: HexFormat = HexFormat.Default): String = toHexString(0, size, format) + +/** + * Formats bytes in this array 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 array 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 ByteArray.toHexString( + startIndex: Int = 0, + endIndex: Int = size, + format: HexFormat = HexFormat.Default +): String { + AbstractList.checkBoundsIndexes(startIndex, endIndex, size) + + if (startIndex == endIndex) { + return "" + } + + val digits = if (format.upperCase) UPPER_CASE_HEX_DIGITS else LOWER_CASE_HEX_DIGITS + + val bytesFormat = format.bytes + val bytesPerLine = bytesFormat.bytesPerLine + val bytesPerGroup = bytesFormat.bytesPerGroup + val bytePrefix = bytesFormat.bytePrefix + val byteSuffix = bytesFormat.byteSuffix + val byteSeparator = bytesFormat.byteSeparator + val groupSeparator = bytesFormat.groupSeparator + + val formatLength = formattedStringLength( + totalBytes = endIndex - startIndex, + bytesPerLine, + bytesPerGroup, + groupSeparator.length, + byteSeparator.length, + bytePrefix.length, + byteSuffix.length + ) + + var indexInLine = 0 + var indexInGroup = 0 + + return buildString(formatLength) { + for (i in startIndex until endIndex) { + val byte = this@toHexString[i].toInt() and 0xFF + + if (indexInLine == bytesPerLine) { + append('\n') + indexInLine = 0 + indexInGroup = 0 + } else if (indexInGroup == bytesPerGroup) { + append(groupSeparator) + indexInGroup = 0 + } + if (indexInGroup != 0) { + append(byteSeparator) + } + + append(bytePrefix) + append(digits[byte shr 4]) + append(digits[byte and 0xF]) + append(byteSuffix) + + indexInGroup += 1 + indexInLine += 1 + } + + check(formatLength == length) + } +} + +// Declared internal for testing +internal fun formattedStringLength( + totalBytes: Int, + bytesPerLine: Int, + bytesPerGroup: Int, + groupSeparatorLength: Int, + byteSeparatorLength: Int, + bytePrefixLength: Int, + byteSuffixLength: Int +): Int { + require(totalBytes > 0) + // By contract bytesPerLine and bytesPerGroup are > 0 + + val lineSeparators = (totalBytes - 1) / bytesPerLine + val groupSeparators = run { + val groupSeparatorsPerLine = (bytesPerLine - 1) / bytesPerGroup + val bytesInLastLine = (totalBytes % bytesPerLine).let { if (it == 0) bytesPerLine else it } + val groupSeparatorsInLastLine = (bytesInLastLine - 1) / bytesPerGroup + lineSeparators * groupSeparatorsPerLine + groupSeparatorsInLastLine + } + val byteSeparators = totalBytes - 1 - lineSeparators - groupSeparators + + // The max totalLength is achieved when + // totalBytes, bytePrefix/Suffix/Separator.length = Int.MAX_VALUE. + // The result is 3 * Int.MAX_VALUE * Int.MAX_VALUE + Int.MAX_VALUE, + // which is > Long.MAX_VALUE, but < ULong.MAX_VALUE. + + val totalLength: Long = lineSeparators.toLong() /* * lineSeparator.length = 1 */ + + groupSeparators.toLong() * groupSeparatorLength.toLong() + + byteSeparators.toLong() * byteSeparatorLength.toLong() + + totalBytes.toLong() * (bytePrefixLength.toLong() + 2L + byteSuffixLength.toLong()) + + if (totalLength !in 0..Int.MAX_VALUE) { + // TODO: Common OutOfMemoryError? + throw IllegalArgumentException("The resulting string length is too big: ${totalLength.toULong()}") + } + + return totalLength.toInt() +} + +/** + * 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.hexToByteArray(format: HexFormat = HexFormat.Default): ByteArray = hexToByteArray(0, length, 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 startIndex the beginning (inclusive) of the substring to parse, 0 by default. + * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. + * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException if the substring does not comply with the specified [format]. + */ +@ExperimentalStdlibApi +//@SinceKotlin("1.9") +private fun String.hexToByteArray( + startIndex: Int = 0, + endIndex: Int = length, + format: HexFormat = HexFormat.Default +): ByteArray { + AbstractList.checkBoundsIndexes(startIndex, endIndex, length) + + if (startIndex == endIndex) { + return byteArrayOf() + } + + val bytesFormat = format.bytes + val bytesPerLine = bytesFormat.bytesPerLine + val bytesPerGroup = bytesFormat.bytesPerGroup + val bytePrefix = bytesFormat.bytePrefix + val byteSuffix = bytesFormat.byteSuffix + val byteSeparator = bytesFormat.byteSeparator + val groupSeparator = bytesFormat.groupSeparator + + val resultCapacity = parsedByteArrayMaxSize( + stringLength = endIndex - startIndex, + bytesPerLine, + bytesPerGroup, + groupSeparator.length, + byteSeparator.length, + bytePrefix.length, + byteSuffix.length + ) + val result = ByteArray(resultCapacity) + + var i = startIndex + var byteIndex = 0 + var indexInLine = 0 + var indexInGroup = 0 + + while (i < endIndex) { + if (indexInLine == bytesPerLine) { + i = checkNewLineAt(i, endIndex) + indexInLine = 0 + indexInGroup = 0 + } else if (indexInGroup == bytesPerGroup) { + i = checkContainsAt(groupSeparator, i, endIndex, "group separator") + indexInGroup = 0 + } else if (indexInGroup != 0) { + i = checkContainsAt(byteSeparator, i, endIndex, "byte separator") + } + indexInLine += 1 + indexInGroup += 1 + + i = checkContainsAt(bytePrefix, i, endIndex, "byte prefix") + + checkHexLength(i, (i + 2).coerceAtMost(endIndex), maxDigits = 2, requireMaxLength = true) + + result[byteIndex++] = ((decimalFromHexDigitAt(i++) shl 4) or decimalFromHexDigitAt(i++)).toByte() + + i = checkContainsAt(byteSuffix, i, endIndex, "byte suffix") + } + + return if (byteIndex == result.size) result else result.copyOf(byteIndex) +} + +// Declared internal for testing +internal fun parsedByteArrayMaxSize( + stringLength: Int, + bytesPerLine: Int, + bytesPerGroup: Int, + groupSeparatorLength: Int, + byteSeparatorLength: Int, + bytePrefixLength: Int, + byteSuffixLength: Int +): Int { + require(stringLength > 0) + // By contract bytesPerLine and bytesPerGroup are > 0 + + // The max charsPerSet is achieved when + // bytesPerLine/Group, bytePrefix/Suffix/SeparatorLength = Int.MAX_VALUE. + // The result is 3 * Int.MAX_VALUE * Int.MAX_VALUE + Int.MAX_VALUE, + // which is > Long.MAX_VALUE, but < ULong.MAX_VALUE. + + val charsPerByte = bytePrefixLength + 2L + byteSuffixLength + + val charsPerGroup = charsPerSet(charsPerByte, bytesPerGroup, byteSeparatorLength) + + val charsPerLine = if (bytesPerLine <= bytesPerGroup) { + charsPerSet(charsPerByte, bytesPerLine, byteSeparatorLength) + } else { + val groupsPerLine = bytesPerLine / bytesPerGroup + var result = charsPerSet(charsPerGroup, groupsPerLine, groupSeparatorLength) + val bytesPerLastGroupInLine = bytesPerLine % bytesPerGroup + if (bytesPerLastGroupInLine != 0) { + result += groupSeparatorLength + result += charsPerSet(charsPerByte, bytesPerLastGroupInLine, byteSeparatorLength) + } + result + } + + var numberOfChars = stringLength.toLong() + + // assume one-character line separator to maximize size + val wholeLines = wholeElementsPerSet(numberOfChars, charsPerLine, 1) + numberOfChars -= wholeLines * (charsPerLine + 1) + + val wholeGroupsInLastLine = wholeElementsPerSet(numberOfChars, charsPerGroup, groupSeparatorLength) + numberOfChars -= wholeGroupsInLastLine * (charsPerGroup + groupSeparatorLength) + + val wholeBytesInLastGroup = wholeElementsPerSet(numberOfChars, charsPerByte, byteSeparatorLength) + numberOfChars -= wholeBytesInLastGroup * (charsPerByte + byteSeparatorLength) + + // If numberOfChars is bigger than zero here: + // * CRLF might have been used as line separator + // * or there are dangling characters at the end of string + // Anyhow, have a spare capacity to let parsing continue. + // In case of dangling characters it will throw later on with a correct message. + val spare = if (numberOfChars > 0L) 1 else 0 + + // The number of parsed bytes will always fit into Int, each parsed byte consumes at least 2 chars of the input string. + return ((wholeLines * bytesPerLine) + (wholeGroupsInLastLine * bytesPerGroup) + wholeBytesInLastGroup + spare).toInt() +} + +private fun charsPerSet(charsPerElement: Long, elementsPerSet: Int, elementSeparatorLength: Int): Long { + require(elementsPerSet > 0) + return (charsPerElement * elementsPerSet) + (elementSeparatorLength * (elementsPerSet - 1L)) +} + +private fun wholeElementsPerSet(charsPerSet: Long, charsPerElement: Long, elementSeparatorLength: Int): Long { + return if (charsPerSet <= 0 || charsPerElement <= 0) 0 + else (charsPerSet + elementSeparatorLength) / (charsPerElement + elementSeparatorLength) +} + +private fun String.checkNewLineAt(index: Int, endIndex: Int): Int { + return if (this[index] == '\r') { + if (index + 1 < endIndex && this[index + 1] == '\n') index + 2 else index + 1 + } else if (this[index] == '\n') { + index + 1 + } else { + throw NumberFormatException("Expected a new line at index $index, but was ${this[index]}") + } +} + +// -------------------------- format and parse Byte -------------------------- + +/** + * Formats this `Byte` value using the specified [format]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.NumberHexFormat] affect formatting. + * + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +public fun Byte.toHexString(format: HexFormat = HexFormat.Default): String = toLong().toHexStringImpl(format, bits = 8) + +/** + * Parses a `Byte` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @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.hexToByte(format: HexFormat = HexFormat.Default): Byte = hexToByte(0, length, format) + +/** + * Parses a `Byte` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @param startIndex the beginning (inclusive) of the substring to parse, 0 by default. + * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. + * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException if the substring does not comply with the specified [format]. + */ +@ExperimentalStdlibApi +//@SinceKotlin("1.9") +private fun String.hexToByte(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Byte = + hexToLongImpl(startIndex, endIndex, format, maxDigits = 2).toByte() + +// -------------------------- format and parse Short -------------------------- + +/** + * Formats this `Short` value using the specified [format]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.NumberHexFormat] affect formatting. + * + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +public fun Short.toHexString(format: HexFormat = HexFormat.Default): String = toLong().toHexStringImpl(format, bits = 16) + +/** + * Parses a `Short` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @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.hexToShort(format: HexFormat = HexFormat.Default): Short = hexToShort(0, length, format) + +/** + * Parses a `Short` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @param startIndex the beginning (inclusive) of the substring to parse, 0 by default. + * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. + * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException if the substring does not comply with the specified [format]. + */ +@ExperimentalStdlibApi +//@SinceKotlin("1.9") +private fun String.hexToShort(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Short = + hexToLongImpl(startIndex, endIndex, format, maxDigits = 4).toShort() + +// -------------------------- format and parse Int -------------------------- + +/** + * Formats this `Int` value using the specified [format]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.NumberHexFormat] affect formatting. + * + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +public fun Int.toHexString(format: HexFormat = HexFormat.Default): String = toLong().toHexStringImpl(format, bits = 32) + +/** + * Parses an `Int` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @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.hexToInt(format: HexFormat = HexFormat.Default): Int = hexToInt(0, length, format) + +/** + * Parses an `Int` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @param startIndex the beginning (inclusive) of the substring to parse, 0 by default. + * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. + * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException if the substring does not comply with the specified [format]. + */ +@ExperimentalStdlibApi +//@SinceKotlin("1.9") +private fun String.hexToInt(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Int = + hexToLongImpl(startIndex, endIndex, format, maxDigits = 8).toInt() + +// -------------------------- format and parse Long -------------------------- + +/** + * Formats this `Long` value using the specified [format]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.NumberHexFormat] affect formatting. + * + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +public fun Long.toHexString(format: HexFormat = HexFormat.Default): String = toHexStringImpl(format, bits = 64) + +/** + * Parses a `Long` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @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.hexToLong(format: HexFormat = HexFormat.Default): Long = hexToLong(0, length, format) + +/** + * Parses a `Long` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @param startIndex the beginning (inclusive) of the substring to parse, 0 by default. + * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. + * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. + * + * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. + * @throws IllegalArgumentException when `startIndex > endIndex`. + * @throws IllegalArgumentException if the substring does not comply with the specified [format]. + */ +@ExperimentalStdlibApi +//@SinceKotlin("1.9") +private fun String.hexToLong(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): Long = + hexToLongImpl(startIndex, endIndex, format, maxDigits = 16) + +// -------------------------- private format and parse functions -------------------------- + +@ExperimentalStdlibApi +private fun Long.toHexStringImpl(format: HexFormat, bits: Int): String { + require(bits and 0x3 == 0) + + val digits = if (format.upperCase) UPPER_CASE_HEX_DIGITS else LOWER_CASE_HEX_DIGITS + val value = this + + val prefix = format.number.prefix + val suffix = format.number.suffix + val formatLength = prefix.length + (bits shr 2) + suffix.length + var removeZeros = format.number.removeLeadingZeros + + return buildString(formatLength) { + append(prefix) + + var shift = bits + while (shift > 0) { + shift -= 4 + val decimal = ((value shr shift) and 0xF).toInt() + removeZeros = removeZeros && decimal == 0 && shift > 0 + if (!removeZeros) { + append(digits[decimal]) + } + } + + append(suffix) + } +} + +@ExperimentalStdlibApi +private fun String.hexToLongImpl(startIndex: Int = 0, endIndex: Int = length, format: HexFormat, maxDigits: Int): Long { + AbstractList.checkBoundsIndexes(startIndex, endIndex, length) + + val prefix = format.number.prefix + val suffix = format.number.suffix + + if (prefix.length + suffix.length >= endIndex - startIndex) { + throw NumberFormatException( + "Expected a hexadecimal number with prefix \"$prefix\" and suffix \"$suffix\", but was ${substring(startIndex, endIndex)}" + ) + } + + val digitsStartIndex = checkContainsAt(prefix, startIndex, endIndex, "prefix") + val digitsEndIndex = endIndex - suffix.length + checkContainsAt(suffix, digitsEndIndex, endIndex, "suffix") + + checkHexLength(digitsStartIndex, digitsEndIndex, maxDigits, requireMaxLength = false) + + var result = 0L + for (i in digitsStartIndex until digitsEndIndex) { + result = (result shl 4) or decimalFromHexDigitAt(i).toLong() + } + return result +} + +private fun String.checkContainsAt(part: String, index: Int, endIndex: Int, partName: String): Int { + val end = index + part.length + if (end > endIndex || !regionMatches(index, part, 0, part.length, ignoreCase = true)) { + throw NumberFormatException( + "Expected $partName \"$part\" at index $index, but was ${this.substring(index, end.coerceAtMost(endIndex))}" + ) + } + return end +} + +private fun String.checkHexLength(startIndex: Int, endIndex: Int, maxDigits: Int, requireMaxLength: Boolean) { + val digitsLength = endIndex - startIndex + val isCorrectLength = if (requireMaxLength) digitsLength == maxDigits else digitsLength <= maxDigits + if (!isCorrectLength) { + val specifier = if (requireMaxLength) "exactly" else "at most" + val substring = substring(startIndex, endIndex) + throw NumberFormatException( + "Expected $specifier $maxDigits hexadecimal digits at index $startIndex, but was $substring of length $digitsLength" + ) + } +} + +private fun String.decimalFromHexDigitAt(index: Int): Int { + val code = this[index].code + if (code > 127 || HEX_DIGITS_TO_DECIMAL[code] < 0) { + throw NumberFormatException("Expected a hexadecimal digit at index $index, but was ${this[index]}") + } + return HEX_DIGITS_TO_DECIMAL[code] +} diff --git a/libraries/stdlib/src/kotlin/text/HexFormat.kt b/libraries/stdlib/src/kotlin/text/HexFormat.kt new file mode 100644 index 00000000000..62616a58f20 --- /dev/null +++ b/libraries/stdlib/src/kotlin/text/HexFormat.kt @@ -0,0 +1,411 @@ +/* + * 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/LICENSE.txt file. + */ + +package kotlin.text + +import kotlin.internal.InlineOnly + +/** + * Represents hexadecimal format options. + * + * To create a new [HexFormat] use `HexFormat` function. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +public class HexFormat internal constructor( + /** + * Specifies whether upper case hexadecimal digits `0-9`, `A-F` should be used for formatting. + * If `false`, lower case hexadecimal digits `0-9`, `a-f` will be used. + * + * Affects both `ByteArray` and numeric value formatting. + */ + val upperCase: Boolean, + /** + * Specifies hexadecimal format used for formatting and parsing `ByteArray`. + */ + val bytes: BytesHexFormat, + /** + * Specifies hexadecimal format used for formatting and parsing a numeric value. + */ + val number: NumberHexFormat +) { + + override fun toString(): String = buildString { + append("HexFormat(").appendLine() + append(" upperCase = ").append(upperCase).appendLine(",") + append(" bytes = BytesHexFormat(").appendLine() + bytes.appendOptionsTo(this, indent = " ").appendLine() + append(" ),").appendLine() + append(" number = NumberHexFormat(").appendLine() + number.appendOptionsTo(this, indent = " ").appendLine() + append(" )").appendLine() + append(")") + } + + /** + * Represents hexadecimal format options for formatting and parsing `ByteArray`. + * + * When formatting one can assume that bytes are firstly separated using LF character (`'\n'`) into lines + * with [bytesPerLine] bytes in each line. The last line may have fewer bytes. + * Then each line is separated into groups using [groupSeparator] with [bytesPerGroup] bytes in each group, + * except the last group in the line, which may have fewer bytes. + * All bytes in a group are separated using [byteSeparator]. + * Each byte is converted to its two-digit hexadecimal representation, + * immediately preceded by [bytePrefix] and immediately succeeded by [byteSuffix]. + * + * When parsing the input string is required to be in the format described above. + * However, any of the char sequences CRLF, LF and CR is considered a valid line separator, + * and parsing is performed in case-insensitive manner. + * + * See [BytesHexFormat.Builder] to find out how the options are configured, + * and what is the default value of each option. + */ + public class BytesHexFormat internal constructor( + /** The maximum number of bytes per line. */ + val bytesPerLine: Int, + + /** The maximum number of bytes per group. */ + val bytesPerGroup: Int, + /** The string used to separate adjacent groups in a line. */ + val groupSeparator: String, + + /** The string used to separate adjacent bytes in a group. */ + val byteSeparator: String, + /** The string that immediately precedes two-digit hexadecimal representation of each byte. */ + val bytePrefix: String, + /** The string that immediately succeeds two-digit hexadecimal representation of each byte. */ + val byteSuffix: String + ) { + + override fun toString(): String = buildString { + append("BytesHexFormat(").appendLine() + appendOptionsTo(this, indent = " ").appendLine() + append(")") + } + + internal fun appendOptionsTo(sb: StringBuilder, indent: String): StringBuilder { + sb.append(indent).append("bytesPerLine = ").append(bytesPerLine).appendLine(",") + sb.append(indent).append("bytesPerGroup = ").append(bytesPerGroup).appendLine(",") + sb.append(indent).append("groupSeparator = \"").append(groupSeparator).appendLine("\",") + sb.append(indent).append("byteSeparator = \"").append(byteSeparator).appendLine("\",") + sb.append(indent).append("bytePrefix = \"").append(bytePrefix).appendLine("\",") + sb.append(indent).append("byteSuffix = \"").append(byteSuffix).append("\"") + return sb + } + + /** + * A context for building a [BytesHexFormat]. Provides API for configuring format options. + */ + class Builder internal constructor() { + /** + * Defines [BytesHexFormat.bytesPerLine] of the format being built, [Int.MAX_VALUE] by default. + * + * The value must be positive. + * + * @throws IllegalArgumentException if a non-positive value is assigned to this property. + */ + var bytesPerLine: Int = Default.bytesPerLine + set(value) { + if (value <= 0) + throw IllegalArgumentException("Non-positive values are prohibited for bytesPerLine, but was $value") + field = value + } + + /** + * Defines [BytesHexFormat.bytesPerGroup] of the format being built, [Int.MAX_VALUE] by default. + * + * The value must be positive. + * + * @throws IllegalArgumentException if a non-positive value is assigned to this property. + */ + var bytesPerGroup: Int = Default.bytesPerGroup + set(value) { + if (value <= 0) + throw IllegalArgumentException("Non-positive values are prohibited for bytesPerGroup, but was $value") + field = value + } + + /** Defines [BytesHexFormat.groupSeparator] of the format being built, two space characters (`" "`) by default. */ + var groupSeparator: String = Default.groupSeparator + + /** + * Defines [BytesHexFormat.byteSeparator] of the format being built, empty string by default. + * + * The string must not contain LF and CR characters. + * + * @throws IllegalArgumentException if a string containing LF or CR character is assigned to this property. + */ + var byteSeparator: String = Default.byteSeparator + set(value) { + if (value.contains('\n') || value.contains('\r')) + throw IllegalArgumentException("LF and CR characters are prohibited in byteSeparator, but was $value") + field = value + } + + /** + * Defines [BytesHexFormat.bytePrefix] of the format being built, empty string by default. + * + * The string must not contain LF and CR characters. + * + * @throws IllegalArgumentException if a string containing LF or CR character is assigned to this property. + */ + var bytePrefix: String = Default.bytePrefix + set(value) { + if (value.contains('\n') || value.contains('\r')) + throw IllegalArgumentException("LF and CR characters are prohibited in bytePrefix, but was $value") + field = value + } + + /** + * Defines [BytesHexFormat.byteSuffix] of the format being built, empty string by default. + * + * The string must not contain LF and CR characters. + * + * @throws IllegalArgumentException if a string containing LF or CR character is assigned to this property. + */ + var byteSuffix: String = Default.byteSuffix + set(value) { + if (value.contains('\n') || value.contains('\r')) + throw IllegalArgumentException("LF and CR characters are prohibited in byteSuffix, but was $value") + field = value + } + + internal fun build(): BytesHexFormat { + return BytesHexFormat(bytesPerLine, bytesPerGroup, groupSeparator, byteSeparator, bytePrefix, byteSuffix) + } + } + + internal companion object { + internal val Default = BytesHexFormat( + bytesPerLine = Int.MAX_VALUE, + bytesPerGroup = Int.MAX_VALUE, + groupSeparator = " ", + byteSeparator = "", + bytePrefix = "", + byteSuffix = "" + ) + } + } + + /** + * Represents hexadecimal format options for formatting and parsing a numeric value. + * + * The formatting result consist of [prefix] string, hexadecimal representation of the value being formatted, and [suffix] string. + * Hexadecimal representation of a value is calculated by mapping each four-bit chunk + * of its binary representation to the corresponding hexadecimal digit, starting with the most significant bits. + * [upperCase] determines whether upper case `0-9`, `A-F` or lower case `0-9`, `a-f` hexadecimal digits are used. + * If [removeLeadingZeros] it `true`, leading zeros in the hexadecimal representation are removed. + * + * For example, the binary representation of the `Byte` value `58` is the 8-bit long `00111010`, + * which converts to a hexadecimal representation of `3a` or `3A` depending on [upperCase]. + * Whereas, the binary representation of the `Int` value `58` is the 32-bit long `00000000000000000000000000111010`, + * which converts to a hexadecimal representation of `0000003a` or `0000003A` depending on [upperCase]. + * If [removeLeadingZeros] it `true`, leading zeros in `0000003a` are removed, resulting `3a`. + * + * To convert a value to hexadecimal string of a particular length, + * first convert the value to a type with the corresponding bit size. + * For example, to convert an `Int` value to 4-digit hexadecimal string, + * convert the value `toShort()` before hexadecimal formatting. + * To convert it to hexadecimal string of at most 4 digits + * without leading zeros, set [removeLeadingZeros] to `true` in addition. + * + * Parsing requires [prefix] and [suffix] to be present in the input string, + * and the amount of hexadecimal digits to be at least one and at most the value bit size divided by four. + * Parsing is performed in case-insensitive manner, and [removeLeadingZeros] is ignored as well. + * + * See [NumberHexFormat.Builder] to find out how the options are configured, + * and what is the default value of each option. + */ + public class NumberHexFormat internal constructor( + /** The string that immediately precedes hexadecimal representation of a numeric value. */ + val prefix: String, + /** The string that immediately succeeds hexadecimal representation of a numeric value. */ + val suffix: String, + /** Specifies whether to remove leading zeros in the hexadecimal representation of a numeric value. */ + val removeLeadingZeros: Boolean + ) { + + override fun toString(): String = buildString { + append("NumberHexFormat(").appendLine() + appendOptionsTo(this, indent = " ").appendLine() + append(")") + } + + internal fun appendOptionsTo(sb: StringBuilder, indent: String): StringBuilder { + sb.append(indent).append("prefix = \"").append(prefix).appendLine("\",") + sb.append(indent).append("suffix = \"").append(suffix).appendLine("\",") + sb.append(indent).append("removeLeadingZeros = ").append(removeLeadingZeros) + return sb + } + + /** + * A context for building a [NumberHexFormat]. Provides API for configuring format options. + */ + class Builder internal constructor() { + /** + * Defines [NumberHexFormat.prefix] of the format being built, empty string by default. + * + * The string must not contain LF and CR characters. + * + * @throws IllegalArgumentException if a string containing LF or CR character is assigned to this property. + */ + var prefix: String = Default.prefix + set(value) { + if (value.contains('\n') || value.contains('\r')) + throw IllegalArgumentException("LF and CR characters are prohibited in prefix, but was $value") + field = value + } + + /** + * Defines [NumberHexFormat.suffix] of the format being built, empty string by default. + * + * The string must not contain LF and CR characters. + * + * @throws IllegalArgumentException if a string containing LF or CR character is assigned to this property. + */ + var suffix: String = Default.suffix + set(value) { + if (value.contains('\n') || value.contains('\r')) + throw IllegalArgumentException("LF and CR characters are prohibited in suffix, but was $value") + field = value + } + + /** Defines [NumberHexFormat.removeLeadingZeros] of the format being built, empty string by default. */ + var removeLeadingZeros: Boolean = Default.removeLeadingZeros + + internal fun build(): NumberHexFormat { + return NumberHexFormat(prefix, suffix, removeLeadingZeros) + } + } + + internal companion object { + internal val Default = NumberHexFormat( + prefix = "", + suffix = "", + removeLeadingZeros = false + ) + } + } + + + /** + * A context for building a [HexFormat]. Provides API for configuring format options. + */ + public class Builder @PublishedApi internal constructor() { + /** Defines [HexFormat.upperCase] of the format being built, `false` by default. */ + var upperCase: Boolean = Default.upperCase + + /** + * Defines [HexFormat.bytes] of the format being built. + * + * See [BytesHexFormat.Builder] for default values of the options. + */ + val bytes: BytesHexFormat.Builder + get() { + if (_bytes == null) { + _bytes = BytesHexFormat.Builder() + } + return _bytes!! + } + + private var _bytes: BytesHexFormat.Builder? = null + + /** + * Defines [HexFormat.number] of the format being built. + * + * See [NumberHexFormat.Builder] for default values of the options. + */ + val number: NumberHexFormat.Builder + get() { + if (_number == null) { + _number = NumberHexFormat.Builder() + } + return _number!! + } + + private var _number: NumberHexFormat.Builder? = null + + /** + * Provides a scope for configuring the [HexFormat.bytes] format options. + * + * See [BytesHexFormat.Builder] for default values of the options. + */ + @InlineOnly + inline fun bytes(builderAction: BytesHexFormat.Builder.() -> Unit) { + bytes.builderAction() + } + + /** + * Provides a scope for configuring the [HexFormat.number] format options. + * + * See [NumberHexFormat.Builder] for default values of the options. + */ + @InlineOnly + inline fun number(builderAction: NumberHexFormat.Builder.() -> Unit) { + number.builderAction() + } + + @PublishedApi + internal fun build(): HexFormat { + return HexFormat( + upperCase, + _bytes?.build() ?: BytesHexFormat.Default, + _number?.build() ?: NumberHexFormat.Default + ) + } + } + + + companion object { + /** + * The default hexadecimal format options. + * + * Uses lower case hexadecimal digits `0-9`, `a-f` when formatting + * both `ByteArray` and numeric values. That is [upperCase] is `false`. + * + * No line separator, group separator, byte separator, byte prefix or byte suffix is used + * when formatting or parsing `ByteArray`. That is: + * * [BytesHexFormat.bytesPerLine] is `Int.MAX_VALUE`. + * * [BytesHexFormat.bytesPerGroup] is `Int.MAX_VALUE`. + * * [BytesHexFormat.byteSeparator], [BytesHexFormat.bytePrefix] and [BytesHexFormat.byteSuffix] are empty strings. + * + * No prefix or suffix is used, and no leading zeros in hexadecimal representation are removed + * when formatting or parsing a numeric value. That is: + * * [NumberHexFormat.prefix] and [NumberHexFormat.suffix] are empty strings. + * * [NumberHexFormat.removeLeadingZeros] is `false`. + */ + public val Default: HexFormat = HexFormat( + upperCase = false, + bytes = BytesHexFormat.Default, + number = NumberHexFormat.Default, + ) + + /** + * Uses upper case hexadecimal digits `0-9`, `A-F` when formatting + * both `ByteArray` and numeric values. That is [upperCase] is `true`. + * + * The same as [Default] format in other aspects. + */ + public val UpperCase: HexFormat = HexFormat( + upperCase = true, + bytes = BytesHexFormat.Default, + number = NumberHexFormat.Default, + ) + } +} + +/** + * Builds a new [HexFormat] by configuring its format options using the specified [builderAction], + * and returns the resulting format. + * + * The builder passed as a receiver to the [builderAction] is valid only inside that function. + * Using it outside of the function produces an unspecified behavior. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +@InlineOnly +public inline fun HexFormat(builderAction: HexFormat.Builder.() -> Unit): HexFormat { + return HexFormat.Builder().apply(builderAction).build() +} diff --git a/libraries/stdlib/src/kotlin/text/UHexExtensions.kt b/libraries/stdlib/src/kotlin/text/UHexExtensions.kt new file mode 100644 index 00000000000..61959376d31 --- /dev/null +++ b/libraries/stdlib/src/kotlin/text/UHexExtensions.kt @@ -0,0 +1,287 @@ +/* + * 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/LICENSE.txt file. + */ + +package kotlin.text + +import kotlin.internal.InlineOnly + +// -------------------------- format and parse UByteArray -------------------------- + +/** + * Formats bytes in this array 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") +@ExperimentalUnsignedTypes +@InlineOnly +public inline fun UByteArray.toHexString(format: HexFormat = HexFormat.Default): String = storage.toHexString(format) + +/** + * Formats bytes in this array 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 array 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") +@ExperimentalUnsignedTypes +@InlineOnly +public inline fun UByteArray.toHexString( + startIndex: Int = 0, + endIndex: Int = size, + format: HexFormat = HexFormat.Default +): String = storage.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") +@ExperimentalUnsignedTypes +@InlineOnly +public inline fun String.hexToUByteArray(format: HexFormat = HexFormat.Default): UByteArray = + hexToByteArray(format).asUByteArray() + +///** +// * 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 startIndex the beginning (inclusive) of the substring to parse, 0 by default. +// * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. +// * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. +// * +// * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. +// * @throws IllegalArgumentException when `startIndex > endIndex`. +// * @throws IllegalArgumentException if the substring does not comply with the specified [format]. +// */ +//@ExperimentalStdlibApi +//@SinceKotlin("1.9") +//@ExperimentalUnsignedTypes +//@InlineOnly +//public inline fun String.hexToUByteArray( +// startIndex: Int = 0, +// endIndex: Int = length, +// format: HexFormat = HexFormat.Default +//): UByteArray = hexToByteArray(startIndex, endIndex, format).asUByteArray() + +// -------------------------- format and parse UByte -------------------------- + +/** + * Formats this `UByte` value using the specified [format]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.NumberHexFormat] affect formatting. + * + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +@InlineOnly +public inline fun UByte.toHexString(format: HexFormat = HexFormat.Default): String = data.toHexString(format) + +/** + * Parses an `UByte` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @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") +@InlineOnly +public inline fun String.hexToUByte(format: HexFormat = HexFormat.Default): UByte = hexToByte(format).toUByte() + +///** +// * Parses an `UByte` value from this string using the specified [format]. +// * +// * Note that only [HexFormat.NumberHexFormat] affects parsing, +// * and parsing is performed in case-insensitive manner. +// * +// * @param startIndex the beginning (inclusive) of the substring to parse, 0 by default. +// * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. +// * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. +// * +// * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. +// * @throws IllegalArgumentException when `startIndex > endIndex`. +// * @throws IllegalArgumentException if the substring does not comply with the specified [format]. +// */ +//@ExperimentalStdlibApi +//@SinceKotlin("1.9") +//@InlineOnly +//public inline fun String.hexToUByte(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): UByte = +// hexToByte(startIndex, endIndex, format).toUByte() + +// -------------------------- format and parse UShort -------------------------- + +/** + * Formats this `UShort` value using the specified [format]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.NumberHexFormat] affect formatting. + * + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +@InlineOnly +public inline fun UShort.toHexString(format: HexFormat = HexFormat.Default): String = data.toHexString(format) + +/** + * Parses an `UShort` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @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") +@InlineOnly +public inline fun String.hexToUShort(format: HexFormat = HexFormat.Default): UShort = hexToShort(format).toUShort() + +///** +// * Parses an `UShort` value from this string using the specified [format]. +// * +// * Note that only [HexFormat.NumberHexFormat] affects parsing, +// * and parsing is performed in case-insensitive manner. +// * +// * @param startIndex the beginning (inclusive) of the substring to parse, 0 by default. +// * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. +// * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. +// * +// * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. +// * @throws IllegalArgumentException when `startIndex > endIndex`. +// * @throws IllegalArgumentException if the substring does not comply with the specified [format]. +// */ +//@ExperimentalStdlibApi +//@SinceKotlin("1.9") +//@InlineOnly +//public inline fun String.hexToUShort(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): UShort = +// hexToShort(startIndex, endIndex, format).toUShort() + +// -------------------------- format and parse UInt -------------------------- + +/** + * Formats this `UInt` value using the specified [format]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.NumberHexFormat] affect formatting. + * + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +@InlineOnly +public inline fun UInt.toHexString(format: HexFormat = HexFormat.Default): String = data.toHexString(format) + +/** + * Parses an `UInt` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @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") +@InlineOnly +public inline fun String.hexToUInt(format: HexFormat = HexFormat.Default): UInt = hexToInt(format).toUInt() + +///** +// * Parses an `UInt` value from this string using the specified [format]. +// * +// * Note that only [HexFormat.NumberHexFormat] affects parsing, +// * and parsing is performed in case-insensitive manner. +// * +// * @param startIndex the beginning (inclusive) of the substring to parse, 0 by default. +// * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. +// * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. +// * +// * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. +// * @throws IllegalArgumentException when `startIndex > endIndex`. +// * @throws IllegalArgumentException if the substring does not comply with the specified [format]. +// */ +//@ExperimentalStdlibApi +//@SinceKotlin("1.9") +//@InlineOnly +//public inline fun String.hexToUInt(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): UInt = +// hexToInt(startIndex, endIndex, format).toUInt() + +// -------------------------- format and parse ULong -------------------------- + +/** + * Formats this `ULong` value using the specified [format]. + * + * Note that only [HexFormat.upperCase] and [HexFormat.NumberHexFormat] affect formatting. + * + * @param format the [HexFormat] to use for formatting, [HexFormat.Default] by default. + */ +@ExperimentalStdlibApi +@SinceKotlin("1.9") +@InlineOnly +public inline fun ULong.toHexString(format: HexFormat = HexFormat.Default): String = data.toHexString(format) + +/** + * Parses an `ULong` value from this string using the specified [format]. + * + * Note that only [HexFormat.NumberHexFormat] affects parsing, + * and parsing is performed in case-insensitive manner. + * + * @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") +@InlineOnly +public inline fun String.hexToULong(format: HexFormat = HexFormat.Default): ULong = hexToLong(format).toULong() + +///** +// * Parses an `ULong` value from this string using the specified [format]. +// * +// * Note that only [HexFormat.NumberHexFormat] affects parsing, +// * and parsing is performed in case-insensitive manner. +// * +// * @param startIndex the beginning (inclusive) of the substring to parse, 0 by default. +// * @param endIndex the end (exclusive) of the substring to parse, length of this string by default. +// * @param format the [HexFormat] to use for parsing, [HexFormat.Default] by default. +// * +// * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of this string indices. +// * @throws IllegalArgumentException when `startIndex > endIndex`. +// * @throws IllegalArgumentException if the substring does not comply with the specified [format]. +// */ +//@ExperimentalStdlibApi +//@SinceKotlin("1.9") +//@InlineOnly +//public inline fun String.hexToULong(startIndex: Int = 0, endIndex: Int = length, format: HexFormat = HexFormat.Default): ULong = +// hexToLong(startIndex, endIndex, format).toULong() diff --git a/libraries/stdlib/test/text/BytesHexFormatTest.kt b/libraries/stdlib/test/text/BytesHexFormatTest.kt new file mode 100644 index 00000000000..185df7204ff --- /dev/null +++ b/libraries/stdlib/test/text/BytesHexFormatTest.kt @@ -0,0 +1,569 @@ +/* + * 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/LICENSE.txt file. + */ + +package test.text + +import kotlin.test.* + +class BytesHexFormatTest { + companion object { + val fourBytes = ByteArray(4) { (10 + it).toByte() } + val twentyBytes = ByteArray(20) { it.toByte() } + } + + private fun testFormatAndParse(bytes: ByteArray, expected: String, format: HexFormat) { + assertEquals(expected, bytes.toHexString(format)) + assertContentEquals(bytes, expected.hexToByteArray(format)) + + assertEquals(expected, bytes.asUByteArray().toHexString(format)) + assertContentEquals(bytes.asUByteArray(), expected.hexToUByteArray(format)) + } + + @Test + fun ignoreNumberFormat() { + val format = HexFormat { + number { + prefix = "0x" + suffix = "h" + removeLeadingZeros = true + } + } + + testFormatAndParse(fourBytes, "0a0b0c0d", format) + } + + @Test + fun upperCase() { + testFormatAndParse(fourBytes, "0A0B0C0D", HexFormat { upperCase = true }) + testFormatAndParse(fourBytes, "0A0B0C0D", HexFormat.UpperCase) + } + + @Test + fun lowerCase() { + testFormatAndParse(fourBytes, "0a0b0c0d", HexFormat { upperCase = false }) + } + + @Test + fun defaultCase() { + testFormatAndParse(fourBytes, "0a0b0c0d", HexFormat.Default) + } + + @Test + fun byteSeparatorPrefixSuffix() { + // byteSeparator + testFormatAndParse(fourBytes, "0a 0b 0c 0d", HexFormat { bytes.byteSeparator = " " }) + // bytePrefix + testFormatAndParse(fourBytes, "0x0a0x0b0x0c0x0d", HexFormat { bytes.bytePrefix = "0x" }) + // bytePrefix + testFormatAndParse(fourBytes, "0a;0b;0c;0d;", HexFormat { bytes.byteSuffix = ";" }) + // all together + testFormatAndParse(fourBytes, "0x0a; 0x0b; 0x0c; 0x0d;", HexFormat { + bytes { + byteSeparator = " " + bytePrefix = "0x" + byteSuffix = ";" + } + }) + } + + @Test + fun bytesPerLine() { + // Fewer bytes in the last line + run { + val format = HexFormat { bytes.bytesPerLine = 8 } + val expected = "0001020304050607\n08090a0b0c0d0e0f\n10111213" + testFormatAndParse(twentyBytes, expected, format) + } + + // The last line is not ended by the line separator + run { + val format = HexFormat { bytes.bytesPerLine = 4 } + val expected = "00010203\n04050607\n08090a0b\n0c0d0e0f\n10111213" + testFormatAndParse(twentyBytes, expected, format) + } + } + + @Test + fun bytesPerGroup() { + // The default group separator, and fewer bytes in the last group + run { + val format = HexFormat { bytes.bytesPerGroup = 8 } + val expected = "0001020304050607 08090a0b0c0d0e0f 10111213" + testFormatAndParse(twentyBytes, expected, format) + } + + // The specified group separator + run { + val format = HexFormat { + bytes { + bytesPerGroup = 8 + groupSeparator = "---" + } + } + val expected = "0001020304050607---08090a0b0c0d0e0f---10111213" + testFormatAndParse(twentyBytes, expected, format) + } + + // The last group is not ended by the group separator + run { + val format = HexFormat { bytes.bytesPerGroup = 4 } + val expected = "00010203 04050607 08090a0b 0c0d0e0f 10111213" + testFormatAndParse(twentyBytes, expected, format) + } + } + + @Test + fun bytesPerLineAndBytesPerGroup() { + val format = HexFormat { + upperCase = true + bytes { + bytesPerLine = 10 + bytesPerGroup = 4 + groupSeparator = "---" + byteSeparator = " " + bytePrefix = "#" + byteSuffix = ";" + } + } + + val byteArray = ByteArray(31) { it.toByte() } + + val expected = """ + #00; #01; #02; #03;---#04; #05; #06; #07;---#08; #09; + #0A; #0B; #0C; #0D;---#0E; #0F; #10; #11;---#12; #13; + #14; #15; #16; #17;---#18; #19; #1A; #1B;---#1C; #1D; + #1E; + """.trimIndent() + + testFormatAndParse(byteArray, expected, format) + } + + @Test + fun macAddress() { + val address = byteArrayOf(0x00, 0x1b, 0x63, 0x84.toByte(), 0x45, 0xe6.toByte()) + + testFormatAndParse( + address, + "00:1b:63:84:45:e6", + HexFormat { bytes.byteSeparator = ":" } + ) + testFormatAndParse( + address, + "00-1B-63-84-45-E6", + HexFormat { upperCase = true; bytes.byteSeparator = "-" } + ) + testFormatAndParse( + address, + "001B.6384.45E6", + HexFormat { upperCase = true; bytes.bytesPerGroup = 2; bytes.groupSeparator = "." } + ) + } + + // Parsing strictness + + @Test + fun parseRequiresTwoDigitsPerByte() { + assertContentEquals(byteArrayOf(58), "3a".hexToByteArray()) + assertFailsWith { + "a".hexToByteArray() + } + assertFailsWith { + "03a".hexToByteArray() + } + + val format = HexFormat { bytes.bytePrefix = "#"; bytes.byteSuffix = ";" } + assertContentEquals(byteArrayOf(58), "#3a;".hexToByteArray(format)) + assertFailsWith { + "#a;".hexToByteArray(format) + } + assertFailsWith { + "#03a;".hexToByteArray(format) + } + + assertFailsWith { + "0a0b0c0".hexToByteArray() + } + } + + @Test + fun parseIgnoresCase() { + assertContentEquals( + twentyBytes, + "000102030405060708090A0B0C0D0E0F10111213".hexToByteArray() + ) + val hexString = "0x00H bs 0x01H bS 0x02H Bs 0x03H gs " + + "0x04h BS 0x05h bs 0x06h Bs 0x07h Gs " + + "0X08H bS 0X09H Bs 0X0aH bs 0X0bH gS " + + "0X0Ch bs 0X0Dh BS 0X0Eh Bs 0X0Fh GS " + + "0x10H Bs 0x11H bS 0x12H BS 0x13H" + val format = HexFormat { + upperCase = true + bytes { + bytesPerGroup = 4 + groupSeparator = " gs " + byteSeparator = " bs " + bytePrefix = "0x" + byteSuffix = "h" + } + } + assertContentEquals( + twentyBytes, + hexString.hexToByteArray(format) + ) + } + + @Test + fun parseAcceptsAllNewLineSequences() { + assertContentEquals( + twentyBytes, + "00010203\n04050607\r\n08090a0b\r0c0d0e0f\r\n10111213".hexToByteArray(HexFormat { bytes.bytesPerLine = 4 }) + ) + } + + @Test + fun parseMultipleNewLines() { + assertFailsWith { + "00010203\n\r04050607\r\n08090a0b\r0c0d0e0f\r\n10111213".hexToByteArray(HexFormat { bytes.bytesPerLine = 4 }) + } + assertFailsWith { + "00010203\n\n04050607\r\n08090a0b\r0c0d0e0f\r\n10111213".hexToByteArray(HexFormat { bytes.bytesPerLine = 4 }) + } + assertFailsWith { + "00010203\n04050607\r\n\n08090a0b\r0c0d0e0f\r\n10111213".hexToByteArray(HexFormat { bytes.bytesPerLine = 4 }) + } + } + + @Test + fun parseNewLineAtEnd() { + assertFailsWith { + "00010203\n04050607\r\n08090a0b\r0c0d0e0f\r\n10111213\n".hexToByteArray(HexFormat { bytes.bytesPerLine = 4 }) + } + } + + // HexFormat corner-case options configuration + + @Test + fun emptyGroupSeparator() { + testFormatAndParse( + twentyBytes, + "000102030405060708090a0b0c0d0e0f10111213", + HexFormat { bytes.bytesPerGroup = 8; bytes.groupSeparator = "" } + ) + testFormatAndParse( + twentyBytes, + "00 01 02 03 04 05 06 0708 09 0a 0b 0c 0d 0e 0f10 11 12 13", + HexFormat { bytes.bytesPerGroup = 8; bytes.groupSeparator = ""; bytes.byteSeparator = " " } + ) + } + + @Test + fun bytesPerGroupBiggerThanBytesPerLine() { + val format = HexFormat { bytes.bytesPerLine = 8; bytes.bytesPerGroup = 10 } + val expected = "0001020304050607\n08090a0b0c0d0e0f\n10111213" + testFormatAndParse(twentyBytes, expected, format) + } + + @Test + fun groupSeparatorWithNewLine() { + val format = HexFormat { bytes.bytesPerLine = 8; bytes.bytesPerGroup = 3; bytes.groupSeparator = "\n" } + val expected = "000102\n030405\n0607\n08090a\n0b0c0d\n0e0f\n101112\n13" + testFormatAndParse(twentyBytes, expected, format) + } + + // Invalid HexFormat configuration + + @Test + fun nonPositiveBytesPerLine() { + assertFailsWith { HexFormat { bytes.bytesPerLine = 0 } } + assertFailsWith { HexFormat { bytes.bytesPerLine = -1 } } + assertFailsWith { HexFormat { bytes.bytesPerLine = Int.MIN_VALUE } } + } + + @Test + fun nonPositiveBytesPerGroup() { + assertFailsWith { HexFormat { bytes.bytesPerGroup = 0 } } + assertFailsWith { HexFormat { bytes.bytesPerGroup = -1 } } + assertFailsWith { HexFormat { bytes.bytesPerGroup = Int.MIN_VALUE } } + } + + @Test + fun byteSeparatorWithNewLine() { + assertFailsWith { HexFormat { bytes.byteSeparator = "ab\n" } } + assertFailsWith { HexFormat { bytes.byteSeparator = "\rcd" } } + } + + @Test + fun bytePrefixWithNewLine() { + assertFailsWith { HexFormat { bytes.bytePrefix = "\n" } } + assertFailsWith { HexFormat { bytes.bytePrefix = "\r" } } + } + + @Test + fun byteSuffixWithNewLine() { + assertFailsWith { HexFormat { bytes.byteSuffix = "ab\n\rcd" } } + assertFailsWith { HexFormat { bytes.byteSuffix = "ab\r\ncd" } } + } + + // format.toString() + + @Test + fun formatToString() { + val format = HexFormat { + upperCase = true + bytes { + bytesPerLine = 10 + bytesPerGroup = 4 + groupSeparator = "---" + byteSeparator = " " + bytePrefix = "#" + byteSuffix = ";" + } + number { + prefix = "0x" + suffix = "h" + removeLeadingZeros = true + } + } + + val expectedHexFormat = """ + HexFormat( + upperCase = true, + bytes = BytesHexFormat( + bytesPerLine = 10, + bytesPerGroup = 4, + groupSeparator = "---", + byteSeparator = " ", + bytePrefix = "#", + byteSuffix = ";" + ), + number = NumberHexFormat( + prefix = "0x", + suffix = "h", + removeLeadingZeros = true + ) + ) + """.trimIndent() + assertEquals(expectedHexFormat, format.toString()) + + val expectedBytesHexFormat = """ + BytesHexFormat( + bytesPerLine = 10, + bytesPerGroup = 4, + groupSeparator = "---", + byteSeparator = " ", + bytePrefix = "#", + byteSuffix = ";" + ) + """.trimIndent() + assertEquals(expectedBytesHexFormat, format.bytes.toString()) + + val expectedNumberHexFormat = """ + NumberHexFormat( + prefix = "0x", + suffix = "h", + removeLeadingZeros = true + ) + """.trimIndent() + assertEquals(expectedNumberHexFormat, format.number.toString()) + } + + // Create format objects on demand + + @Test + fun createOnDemand() { + assertSame(HexFormat.Default.bytes, HexFormat.UpperCase.bytes) + assertSame(HexFormat.Default.number, HexFormat.UpperCase.number) + + val emptyFormat = HexFormat {} + assertNotSame(HexFormat.Default, emptyFormat) + assertEquals(HexFormat.Default.upperCase, emptyFormat.upperCase) + assertSame(HexFormat.Default.bytes, emptyFormat.bytes) + assertSame(HexFormat.Default.number, emptyFormat.number) + + val bytesFormat = HexFormat { bytes } + assertNotSame(HexFormat.Default.bytes, bytesFormat.bytes) + assertSame(HexFormat.Default.number, bytesFormat.number) + + val numberFormat = HexFormat { number {} } + assertSame(HexFormat.Default.bytes, numberFormat.bytes) + assertNotSame(HexFormat.Default.number, numberFormat.number) + } + + @Test + fun formattedStringLength() { + run { +// 00010203\n04050607\n08090a0b\n0c0d0e0f\n10111213 + val length = formattedStringLength( + totalBytes = 20, + bytesPerLine = 4, + bytesPerGroup = Int.MAX_VALUE, + groupSeparatorLength = 2, + byteSeparatorLength = 0, + bytePrefixLength = 0, + byteSuffixLength = 0 + ) + assertEquals(44, length) + } + run { +// 0001020304050607---08090a0b0c0d0e0f---10111213 + val length = formattedStringLength( + totalBytes = 20, + bytesPerLine = Int.MAX_VALUE, + bytesPerGroup = 8, + groupSeparatorLength = 3, + byteSeparatorLength = 0, + bytePrefixLength = 0, + byteSuffixLength = 0 + ) + assertEquals(46, length) + } + run { +// #00; #01; #02; #03;---#04; #05; #06; #07;---#08; #09; +// #0A; #0B; #0C; #0D;---#0E; #0F; #10; #11;---#12; #13; +// #14; #15; #16; #17;---#18; #19; #1A; #1B;---#1C; #1D; +// #1E; + val length = formattedStringLength( + totalBytes = 31, + bytesPerLine = 10, + bytesPerGroup = 4, + groupSeparatorLength = 3, + byteSeparatorLength = 1, + bytePrefixLength = 1, + byteSuffixLength = 1 + ) + assertEquals(166, length) + } + run { + val length = formattedStringLength( + totalBytes = Int.MAX_VALUE / 2, + bytesPerLine = Int.MAX_VALUE, + bytesPerGroup = Int.MAX_VALUE, + groupSeparatorLength = 0, + byteSeparatorLength = 0, + bytePrefixLength = 0, + byteSuffixLength = 0 + ) + assertEquals(Int.MAX_VALUE / 2 * 2, length) + } + assertFailsWith { + formattedStringLength( + totalBytes = Int.MAX_VALUE, + bytesPerLine = Int.MAX_VALUE, + bytesPerGroup = Int.MAX_VALUE, + groupSeparatorLength = Int.MAX_VALUE, + byteSeparatorLength = Int.MAX_VALUE, + bytePrefixLength = Int.MAX_VALUE, + byteSuffixLength = Int.MAX_VALUE + ) + } + assertFailsWith { + formattedStringLength( + totalBytes = Int.MAX_VALUE / 2 + 1, + bytesPerLine = Int.MAX_VALUE, + bytesPerGroup = Int.MAX_VALUE, + groupSeparatorLength = 0, + byteSeparatorLength = 0, + bytePrefixLength = 0, + byteSuffixLength = 0 + ) + } + } + + @Test + fun parsedByteArrayMaxSize() { + run { +// 00010203\n04050607\n08090a0b\n0c0d0e0f\n10111213 + val maxSize = parsedByteArrayMaxSize( + stringLength = 44, + bytesPerLine = 4, + bytesPerGroup = Int.MAX_VALUE, + groupSeparatorLength = 2, + byteSeparatorLength = 0, + bytePrefixLength = 0, + byteSuffixLength = 0 + ) + assertEquals(20, maxSize) + } + run { +// 0001020304050607---08090a0b0c0d0e0f---10111213 + val maxSize = parsedByteArrayMaxSize( + stringLength = 46, + bytesPerLine = Int.MAX_VALUE, + bytesPerGroup = 8, + groupSeparatorLength = 3, + byteSeparatorLength = 0, + bytePrefixLength = 0, + byteSuffixLength = 0 + ) + assertEquals(20, maxSize) + } + run { +// #00; #01; #02; #03;---#04; #05; #06; #07;---#08; #09; +// #0A; #0B; #0C; #0D;---#0E; #0F; #10; #11;---#12; #13; +// #14; #15; #16; #17;---#18; #19; #1A; #1B;---#1C; #1D; +// #1E; + val maxSize = parsedByteArrayMaxSize( + stringLength = 166, + bytesPerLine = 10, + bytesPerGroup = 4, + groupSeparatorLength = 3, + byteSeparatorLength = 1, + bytePrefixLength = 1, + byteSuffixLength = 1 + ) + assertEquals(31, maxSize) + } + run { +// 0001020304050607\n08090a0b0c0d0e0f\n10111213 + // bytesPerGroup > bytesPerLine + val maxSize = parsedByteArrayMaxSize( + stringLength = 42, + bytesPerLine = 8, + bytesPerGroup = 10, + groupSeparatorLength = 3, + byteSeparatorLength = 0, + bytePrefixLength = 0, + byteSuffixLength = 0 + ) + assertEquals(20, maxSize) + } + run { + val maxSize = parsedByteArrayMaxSize( + stringLength = Int.MAX_VALUE, + bytesPerLine = Int.MAX_VALUE, + bytesPerGroup = Int.MAX_VALUE, + groupSeparatorLength = 0, + byteSeparatorLength = 0, + bytePrefixLength = 0, + byteSuffixLength = 0 + ) + assertEquals(Int.MAX_VALUE / 2 + 1, maxSize) + } + run { + val maxSize = parsedByteArrayMaxSize( + stringLength = Int.MAX_VALUE, + bytesPerLine = Int.MAX_VALUE, + bytesPerGroup = Int.MAX_VALUE, + groupSeparatorLength = Int.MAX_VALUE, + byteSeparatorLength = Int.MAX_VALUE, + bytePrefixLength = Int.MAX_VALUE, + byteSuffixLength = Int.MAX_VALUE + ) + assertEquals(1, maxSize) + } + run { + val maxSize = parsedByteArrayMaxSize( + stringLength = Int.MAX_VALUE, + bytesPerLine = Int.MAX_VALUE, + bytesPerGroup = Int.MAX_VALUE / 2, + groupSeparatorLength = Int.MAX_VALUE, + byteSeparatorLength = Int.MAX_VALUE, + bytePrefixLength = Int.MAX_VALUE, + byteSuffixLength = Int.MAX_VALUE + ) + assertEquals(1, maxSize) + } + } +} \ No newline at end of file diff --git a/libraries/stdlib/test/text/NumberHexFormatTest.kt b/libraries/stdlib/test/text/NumberHexFormatTest.kt new file mode 100644 index 00000000000..a3445732014 --- /dev/null +++ b/libraries/stdlib/test/text/NumberHexFormatTest.kt @@ -0,0 +1,211 @@ +/* + * 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/LICENSE.txt file. + */ + +package test.text + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class NumberHexFormatTest { + + companion object { + private const val longHex3a: String = "000000000000003a" // 16 hex digits + private const val longHex3A: String = "000000000000003A" // 16 hex digits + } + + private fun testFormatAndParse(number: Long, string: String, format: HexFormat) { + testFormat(number, string, format) + testParse(string, number, format) + } + + private fun testFormat(number: Long, string: String, format: HexFormat) { + require(format.number.prefix.isEmpty() && format.number.suffix.isEmpty()) + + assertEquals(string.takeLast(2), number.toByte().toHexString(format)) + assertEquals(string.takeLast(2), number.toUByte().toHexString(format)) + assertEquals(string.takeLast(4), number.toShort().toHexString(format)) + assertEquals(string.takeLast(4), number.toUShort().toHexString(format)) + assertEquals(string.takeLast(8), number.toInt().toHexString(format)) + assertEquals(string.takeLast(8), number.toUInt().toHexString(format)) + assertEquals(string, number.toHexString(format)) + assertEquals(string, number.toULong().toHexString(format)) + } + + private fun testParse(string: String, number: Long, format: HexFormat) { + require(format.number.prefix.isEmpty() && format.number.suffix.isEmpty()) + + assertEquals(number.toByte(), string.takeLast(2).hexToByte(format)) + assertEquals(number.toUByte(), string.takeLast(2).hexToUByte(format)) + assertEquals(number.toShort(), string.takeLast(4).hexToShort(format)) + assertEquals(number.toUShort(), string.takeLast(4).hexToUShort(format)) + assertEquals(number.toInt(), string.takeLast(8).hexToInt(format)) + assertEquals(number.toUInt(), string.takeLast(8).hexToUInt(format)) + assertEquals(number, string.hexToLong(format)) + assertEquals(number.toULong(), string.hexToULong(format)) + } + + @Test + fun ignoreBytesFormat() { + val format = HexFormat { + bytes { + bytesPerLine = 10 + bytesPerGroup = 4 + groupSeparator = "---" + byteSeparator = " " + bytePrefix = "#" + byteSuffix = ";" + } + } + + testFormatAndParse(58, longHex3a, format) + } + + @Test + fun upperCase() { + testFormatAndParse(58, longHex3A, HexFormat { upperCase = true }) + testFormatAndParse(58, longHex3A, HexFormat.UpperCase) + } + + @Test + fun lowerCase() { + testFormatAndParse(58, longHex3a, HexFormat { upperCase = false }) + } + + @Test + fun defaultCase() { + testFormatAndParse(58, longHex3a, HexFormat.Default) + } + + @Test + fun formatAndParseZero() { + testFormatAndParse(0, "0".repeat(16), HexFormat.Default) + testParse("0", 0, HexFormat.Default) + } + + @Test + fun formatAndParseMax() { + testFormatAndParse(Long.MAX_VALUE, "7" + "f".repeat(15), HexFormat.Default) + testFormatAndParse(-1, "f".repeat(16), HexFormat.Default) + } + + @Test + fun formatPrefixSuffix() { + assertEquals("0x0000003a", 58.toHexString(HexFormat { number.prefix = "0x" })) + assertEquals("0000003ah", 58.toHexString(HexFormat { number.suffix = "h" })) + assertEquals("0x0000003ah", 58.toHexString(HexFormat { number.prefix = "0x"; number.suffix = "h" })) + } + + @Test + fun removeLeadingZeros() { + val format = HexFormat { number.removeLeadingZeros = true } + testFormatAndParse(58, "3a", format) + testFormatAndParse(0, "0", format) + } + + @Test + fun parseLongFromSubstring() { + val url = "https://magnuschatt.medium.com/why-you-should-totally-switch-to-kotlin-c7bbde9e10d5" + val articleId = url.substringAfterLast('-').hexToLong() + assertEquals(0xc7bbde9e10d5, articleId) + } + + // Number parsing strictness + + @Test + fun parseRequiresPrefixSuffix() { + assertEquals(58, "0x0000003a".hexToInt(HexFormat { number.prefix = "0x" })) + assertEquals(58, "0x3a".hexToInt(HexFormat { number.prefix = "0x" })) + assertEquals(10, "3a".hexToInt(HexFormat { number.prefix = "3" })) + assertFailsWith { + "0000003a".hexToInt(HexFormat { number.prefix = "0x" }) + } + assertEquals(58, "0000003ah".hexToInt(HexFormat { number.suffix = "h" })) + assertEquals(58, "3ah".hexToInt(HexFormat { number.suffix = "h" })) + assertEquals(3, "3a".hexToInt(HexFormat { number.suffix = "a" })) + assertFailsWith { + "0000003a".hexToInt(HexFormat { number.suffix = "h" }) + } + } + + @Test + fun parseRequiresAtLeastOneHexDigit() { + assertFailsWith { + "".hexToInt() + } + assertFailsWith { + "3a".hexToInt(HexFormat { number { prefix = "3"; suffix = "a" } }) + } + assertFailsWith { + "3a".hexToInt(HexFormat { number.prefix = "3a" }) + } + assertFailsWith { + "3a".hexToInt(HexFormat { number.suffix = "3a" }) + } + assertEquals(15, "0xfh".hexToInt(HexFormat { number { prefix = "0x"; suffix = "h" } })) + } + + @Test + fun parseIgnoresCase() { + assertEquals(58, "0000003a".hexToInt()) + assertEquals(58, "3a".hexToInt()) + assertEquals(58, "0000003A".hexToInt()) + assertEquals(58, "3A".hexToInt()) + + val format = HexFormat { + upperCase = true + number { + prefix = "0X" + suffix = "h" + } + } + assertEquals(58, "0X0000003AH".hexToInt(format)) + assertEquals(58, "0x3Ah".hexToInt(format)) + assertEquals(58, "0X0000003aH".hexToInt(format)) + assertEquals(58, "0x3ah".hexToInt(format)) + } + + @Test + fun parseIgnoresRemoveLeadingZeros() { + assertEquals(58, "3a".hexToInt()) + assertEquals(58, "3a".hexToInt(HexFormat { number.removeLeadingZeros = false })) + assertEquals(58, "0000003a".hexToInt(HexFormat { number.removeLeadingZeros = true })) + } + + @Test + fun parseLimitsHexLength() { + testParse("3", 3, HexFormat.Default) // length = 1 + testParse("00", 0, HexFormat.Default) // length = 2 + testParse("3a", 58, HexFormat.Default) // length = 2 + assertFailsWith { "03a".hexToByte() } // length = 3 + assertFailsWith { "03a".hexToUByte() } // length = 3 + testParse("03a", 58, HexFormat.Default) // length = 3 + testParse("003a", 58, HexFormat.Default) // length = 4 + assertFailsWith { "0003a".hexToShort() } // length = 5 + assertFailsWith { "0003a".hexToUShort() } // length = 5 + testParse("0003a", 58, HexFormat.Default) // length = 5 + testParse("0000003a", 58, HexFormat.Default) // length = 8 + assertFailsWith { "00000003a".hexToInt() } // length = 9 + assertFailsWith { "00000003a".hexToUInt() } // length = 9 + testParse("00000003a", 58, HexFormat.Default) // length = 9 + testParse("000000000000003a", 58, HexFormat.Default) // length = 16 + assertFailsWith { "00000000000000003a".hexToLong() } // length = 17 + assertFailsWith { "00000000000000003a".hexToULong() } // length = 17 + } + + // Invalid HexFormat configuration + + @Test + fun prefixWithNewLine() { + assertFailsWith { HexFormat { number.prefix = "\n" } } + assertFailsWith { HexFormat { number.prefix = "\r" } } + } + + @Test + fun suffixWithNewLine() { + assertFailsWith { HexFormat { number.suffix = "ab\ncd" } } + assertFailsWith { HexFormat { number.suffix = "ab\rcd" } } + } +} \ No newline at end of file diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index 5088b3c8e76..f1c5dc64a70 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -5456,6 +5456,94 @@ public final class kotlin/text/Charsets { public final fun UTF32_LE ()Ljava/nio/charset/Charset; } +public final class kotlin/text/HexExtensionsKt { + public static final fun hexToByte (Ljava/lang/String;Lkotlin/text/HexFormat;)B + public static synthetic fun hexToByte$default (Ljava/lang/String;Lkotlin/text/HexFormat;ILjava/lang/Object;)B + public static final fun hexToByteArray (Ljava/lang/String;Lkotlin/text/HexFormat;)[B + public static synthetic fun hexToByteArray$default (Ljava/lang/String;Lkotlin/text/HexFormat;ILjava/lang/Object;)[B + public static final fun hexToInt (Ljava/lang/String;Lkotlin/text/HexFormat;)I + public static synthetic fun hexToInt$default (Ljava/lang/String;Lkotlin/text/HexFormat;ILjava/lang/Object;)I + public static final fun hexToLong (Ljava/lang/String;Lkotlin/text/HexFormat;)J + public static synthetic fun hexToLong$default (Ljava/lang/String;Lkotlin/text/HexFormat;ILjava/lang/Object;)J + public static final fun hexToShort (Ljava/lang/String;Lkotlin/text/HexFormat;)S + public static synthetic fun hexToShort$default (Ljava/lang/String;Lkotlin/text/HexFormat;ILjava/lang/Object;)S + public static final fun toHexString (BLkotlin/text/HexFormat;)Ljava/lang/String; + public static final fun toHexString (ILkotlin/text/HexFormat;)Ljava/lang/String; + public static final fun toHexString (JLkotlin/text/HexFormat;)Ljava/lang/String; + public static final fun toHexString (SLkotlin/text/HexFormat;)Ljava/lang/String; + public static final fun toHexString ([BIILkotlin/text/HexFormat;)Ljava/lang/String; + public static final fun toHexString ([BLkotlin/text/HexFormat;)Ljava/lang/String; + public static synthetic fun toHexString$default (BLkotlin/text/HexFormat;ILjava/lang/Object;)Ljava/lang/String; + public static synthetic fun toHexString$default (ILkotlin/text/HexFormat;ILjava/lang/Object;)Ljava/lang/String; + public static synthetic fun toHexString$default (JLkotlin/text/HexFormat;ILjava/lang/Object;)Ljava/lang/String; + public static synthetic fun toHexString$default (SLkotlin/text/HexFormat;ILjava/lang/Object;)Ljava/lang/String; + public static synthetic fun toHexString$default ([BIILkotlin/text/HexFormat;ILjava/lang/Object;)Ljava/lang/String; + public static synthetic fun toHexString$default ([BLkotlin/text/HexFormat;ILjava/lang/Object;)Ljava/lang/String; +} + +public final class kotlin/text/HexFormat { + public static final field Companion Lkotlin/text/HexFormat$Companion; + public final fun getBytes ()Lkotlin/text/HexFormat$BytesHexFormat; + public final fun getNumber ()Lkotlin/text/HexFormat$NumberHexFormat; + public final fun getUpperCase ()Z + public fun toString ()Ljava/lang/String; +} + +public final class kotlin/text/HexFormat$Builder { + public fun ()V + public final fun build ()Lkotlin/text/HexFormat; + public final fun getBytes ()Lkotlin/text/HexFormat$BytesHexFormat$Builder; + public final fun getNumber ()Lkotlin/text/HexFormat$NumberHexFormat$Builder; + public final fun getUpperCase ()Z + public final fun setUpperCase (Z)V +} + +public final class kotlin/text/HexFormat$BytesHexFormat { + public final fun getBytePrefix ()Ljava/lang/String; + public final fun getByteSeparator ()Ljava/lang/String; + public final fun getByteSuffix ()Ljava/lang/String; + public final fun getBytesPerGroup ()I + public final fun getBytesPerLine ()I + public final fun getGroupSeparator ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class kotlin/text/HexFormat$BytesHexFormat$Builder { + public final fun getBytePrefix ()Ljava/lang/String; + public final fun getByteSeparator ()Ljava/lang/String; + public final fun getByteSuffix ()Ljava/lang/String; + public final fun getBytesPerGroup ()I + public final fun getBytesPerLine ()I + public final fun getGroupSeparator ()Ljava/lang/String; + public final fun setBytePrefix (Ljava/lang/String;)V + public final fun setByteSeparator (Ljava/lang/String;)V + public final fun setByteSuffix (Ljava/lang/String;)V + public final fun setBytesPerGroup (I)V + public final fun setBytesPerLine (I)V + public final fun setGroupSeparator (Ljava/lang/String;)V +} + +public final class kotlin/text/HexFormat$Companion { + public final fun getDefault ()Lkotlin/text/HexFormat; + public final fun getUpperCase ()Lkotlin/text/HexFormat; +} + +public final class kotlin/text/HexFormat$NumberHexFormat { + public final fun getPrefix ()Ljava/lang/String; + public final fun getRemoveLeadingZeros ()Z + public final fun getSuffix ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class kotlin/text/HexFormat$NumberHexFormat$Builder { + public final fun getPrefix ()Ljava/lang/String; + public final fun getRemoveLeadingZeros ()Z + public final fun getSuffix ()Ljava/lang/String; + public final fun setPrefix (Ljava/lang/String;)V + public final fun setRemoveLeadingZeros (Z)V + public final fun setSuffix (Ljava/lang/String;)V +} + public final class kotlin/text/MatchGroup { public fun (Ljava/lang/String;Lkotlin/ranges/IntRange;)V public final fun component1 ()Ljava/lang/String;