From d176b43eff571d84f050bc5f6a8677480b5ba44b Mon Sep 17 00:00:00 2001 From: Robinson Date: Thu, 13 Jul 2023 22:56:38 +0200 Subject: [PATCH] Hex calculations now support start/length, so hex values can be for a range of bytes. Optimized how hex encoding works --- src/dorkbox/bytes/HexExtensions.kt | 67 +++++++++++++++++++++++------- test/dorkbox/bytes/TestHex.kt | 32 +++++++++----- 2 files changed, 72 insertions(+), 27 deletions(-) diff --git a/src/dorkbox/bytes/HexExtensions.kt b/src/dorkbox/bytes/HexExtensions.kt index b1ade1e..0b48ccb 100644 --- a/src/dorkbox/bytes/HexExtensions.kt +++ b/src/dorkbox/bytes/HexExtensions.kt @@ -51,8 +51,9 @@ object Hex { /** * Represents all the chars used for nibble */ - private const val CHARS = "0123456789abcdef" - private const val CHARS_UPPER = "0123456789ABCDEF" + private val HEX_CHARS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') + private val UPPER_HEX_CHARS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') + internal val HEX_REGEX = Regex("(0[xX])?[0-9a-fA-F]*") @@ -60,14 +61,24 @@ object Hex { * Encodes the given byte value as a hexadecimal character. */ fun encode(value: Byte): String { - return CHARS[value.toInt().shr(4) and 0x0f].toString() + CHARS[value.toInt().and(0x0f)].toString() + val hexString = CharArray(2) + val toInt = value.toInt() + hexString[0] = HEX_CHARS[toInt and 0xF0 shr 4] + hexString[1] = HEX_CHARS[toInt and 0x0F] + + return String(hexString) } /** * Encodes the given byte value as a hexadecimal character. */ fun encodeUpper(value: Byte): String { - return CHARS_UPPER[value.toInt().shr(4) and 0x0f].toString() + CHARS_UPPER[value.toInt().and(0x0f)].toString() + val hexString = CharArray(2) + val toInt = value.toInt() + hexString[0] = UPPER_HEX_CHARS[toInt and 0xF0 shr 4] + hexString[1] = UPPER_HEX_CHARS[toInt and 0x0F] + + return String(hexString) } /** @@ -76,16 +87,40 @@ object Hex { * Note that by default the 0x prefix is prepended to the result of the conversion. * If you want to have the representation without the 0x prefix, pass to this method an empty prefix. */ - fun encode(value: ByteArray, prefix: String = "0x", limit: Int = value.size, toUpperCase: Boolean = false): String { - return if (toUpperCase) { - prefix + value.joinToString(separator = "", limit = limit, truncated = "") { encodeUpper(it) } - } else { - prefix + value.joinToString(separator = "", limit = limit, truncated = "") { encode(it) } + fun encode(bytes: ByteArray, prefix: String = "0x", start: Int = 0, length: Int = bytes.size, toUpperCase: Boolean = false): String { + require(start >= 0) { "Start ($start) must be >= 0" } + require(length >= 0) { "Limit ($length) must be >= 0" } + require(bytes.isEmpty() || start < bytes.size) { "Start ($start) position must be smaller than the size of the byte array" } + + @Suppress("NAME_SHADOWING") + val length = if (length < bytes.size) length else bytes.size + + val hexString = CharArray(prefix.length + ((2 * (length - start) ))) + if (prefix.isNotEmpty()) { + prefix.toCharArray().copyInto(hexString) } + + var j = prefix.length + + if (toUpperCase) { + for (i in start until length) { + val toInt = bytes[i].toInt() + hexString[j++] = UPPER_HEX_CHARS[toInt and 0xF0 shr 4] + hexString[j++] = UPPER_HEX_CHARS[toInt and 0x0F] + } + } else { + for (i in start until length) { + val toInt = bytes[i].toInt() + hexString[j++] = HEX_CHARS[toInt and 0xF0 shr 4] + hexString[j++] = HEX_CHARS[toInt and 0x0F] + } + } + + return String(hexString) } /** - * Converts the given ch into its integer representation considering it as an hexadecimal character. + * Converts the given ch into its integer representation considering it as a hexadecimal character. */ private fun hexToBin(ch: Char): Int = when (ch) { in '0'..'9' -> ch - '0' @@ -107,8 +142,8 @@ object Hex { throw IllegalArgumentException("hex-string must have an even number of digits (nibbles)") } - // Remove the 0x prefix if it is set - val cleanInput = if (value.startsWith("0x")) value.substring(2) else value + // Remove the 0x or 0X prefix if it is set + val cleanInput = if (value.startsWith("0x") || value.startsWith("0X")) value.substring(2) else value return ByteArray(cleanInput.length / 2).apply { var i = 0 @@ -127,12 +162,12 @@ object Hex { * If you want to have the representation without the 0x prefix, use the [toNoPrefixHexString] method or * pass to this method an empty [prefix]. */ -fun ByteArray.toHexString(prefix: String = "0x", limit: Int = this.size, toUpperCase: Boolean = false): String = Hex.encode(this, prefix, limit, toUpperCase) +fun ByteArray.toHexString(prefix: String = "0x", start: Int = 0, length: Int = this.size, toUpperCase: Boolean = false): String = Hex.encode(this, prefix, start, length, toUpperCase) /** * Converts [this] [ByteArray] into its hexadecimal representation without prepending any prefix to it. */ -fun ByteArray.toNoPrefixHexString(limit: Int = this.size, toUpperCase: Boolean = false): String = Hex.encode(this, "", limit, toUpperCase) +fun ByteArray.toNoPrefixHexString(start: Int = 0, length: Int = this.size, toUpperCase: Boolean = false): String = Hex.encode(this, "", start, length, toUpperCase) /** @@ -142,12 +177,12 @@ fun ByteArray.toNoPrefixHexString(limit: Int = this.size, toUpperCase: Boolean = * If you want to have the representation without the 0x prefix, use the [toNoPrefixHexString] method or * pass to this method an empty [prefix]. */ -fun Collection.toHexString(prefix: String = "0x", limit: Int = this.size): String = Hex.encode(this.toByteArray(), prefix, limit) +fun Collection.toHexString(prefix: String = "0x", length: Int = this.size): String = Hex.encode(this.toByteArray(), prefix, length) /** * Converts [this] [Collection] of bytes into its hexadecimal representation without prepending any prefix to it. */ -fun Collection.toNoPrefixHexString(limit: Int = this.size): String = Hex.encode(this.toByteArray(), "", limit) +fun Collection.toNoPrefixHexString(length: Int = this.size): String = Hex.encode(this.toByteArray(), "", length) /** diff --git a/test/dorkbox/bytes/TestHex.kt b/test/dorkbox/bytes/TestHex.kt index 015560e..e6e9c07 100644 --- a/test/dorkbox/bytes/TestHex.kt +++ b/test/dorkbox/bytes/TestHex.kt @@ -54,17 +54,27 @@ class TestHex { @Test fun byteArrayLimitWorks() { - assertEquals("0x", Hex.encode(Hex.decode("00"), limit = 0)) - assertEquals("0x00", Hex.encode(Hex.decode("00"), limit = 1)) - assertEquals("0x", Hex.encode(Hex.decode("ff"), limit = 0)) - assertEquals("0xff", Hex.encode(Hex.decode("ff"), limit = 1)) - assertEquals("0x", Hex.encode(Hex.decode("abcdef"), limit = 0)) - assertEquals("0xab", Hex.encode(Hex.decode("abcdef"), limit = 1)) - assertEquals("0xabcd", Hex.encode(Hex.decode("abcdef"), limit = 2)) - assertEquals("0xabcdef", Hex.encode(Hex.decode("abcdef"), limit = 3)) - assertEquals("0xabcdef", Hex.encode(Hex.decode("abcdef"), limit = 32)) - assertEquals("0xaa12456789bb", Hex.encode(Hex.decode("0xaa12456789bb"), limit = 6)) - assertEquals("0xaa12456789bb", Hex.encode(Hex.decode("0xaa12456789bb"), limit = 9)) + assertEquals("0x", Hex.encode(Hex.decode("00"), length = 0)) + assertEquals("0x00", Hex.encode(Hex.decode("00"), length = 1)) + assertEquals("0x", Hex.encode(Hex.decode("ff"), length = 0)) + assertEquals("0xff", Hex.encode(Hex.decode("ff"), length = 1)) + assertEquals("0x", Hex.encode(Hex.decode("abcdef"), length = 0)) + assertEquals("0xab", Hex.encode(Hex.decode("abcdef"), length = 1)) + assertEquals("0xabcd", Hex.encode(Hex.decode("abcdef"), length = 2)) + assertEquals("0xabcdef", Hex.encode(Hex.decode("abcdef"), length = 3)) + assertEquals("0xabcdef", Hex.encode(Hex.decode("abcdef"), length = 32)) + assertEquals("0xaa12456789bb", Hex.encode(Hex.decode("0xaa12456789bb"), length = 6)) + assertEquals("0xaa12456789bb", Hex.encode(Hex.decode("0xaa12456789bb"), length = 9)) + } + + @Test + fun byteArrayStartWorks() { + assertEquals("0x", Hex.encode(Hex.decode("abcdef"), start = 1, length = 1)) + assertEquals("0xcd", Hex.encode(Hex.decode("abcdef"), start = 1, length = 2)) + assertEquals("0xcdef", Hex.encode(Hex.decode("abcdef"), start = 1, length = 3)) + assertEquals("0xcdef", Hex.encode(Hex.decode("abcdef"), start = 1, length = 32)) + assertEquals("0x6789bb", Hex.encode(Hex.decode("0xaa12456789bb"), start = 3, length = 6)) + assertEquals("0x456789bb", Hex.encode(Hex.decode("0xaa12456789bb"), start = 2, length = 9)) } @Test