Introduce HexFormat for formatting and parsing hexadecimals #KT-57762

Merge-request: KT-MR-9460
Merged-by: Abduqodiri Qurbonzoda <abduqodiri.qurbonzoda@jetbrains.com>
This commit is contained in:
Abduqodiri Qurbonzoda
2023-04-26 09:54:25 +00:00
committed by Space Team
parent 5ef0b52ac5
commit 63a5a74613
14 changed files with 2539 additions and 6 deletions
@@ -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]
}
@@ -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()
}
@@ -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()