Added hex

This commit is contained in:
Robinson 2021-08-22 23:32:41 -06:00
parent 57e9f3ffd3
commit 4a22e3335f
3 changed files with 325 additions and 0 deletions

90
src/dorkbox/bytes/Hex.kt Normal file
View File

@ -0,0 +1,90 @@
/*
* MIT License
*
* Copyright (c) 2017 ligi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package dorkbox.bytes
@JvmInline
public value class HexString(val string: String)
object Hex {
/**
* Represents all the chars used for nibble
*/
private const val CHARS = "0123456789abcdef"
internal val HEX_REGEX = Regex("(0[xX])?[0-9a-fA-F]*")
/**
* Encodes the given byte value as an hexadecimal character.
*/
fun encode(value: Byte): String {
return CHARS[value.toInt().shr(4) and 0x0f].toString() + CHARS[value.toInt().and(0x0f)].toString()
}
/**
* Encodes the given byte array value to its hexadecimal representations, and prepends the given prefix to it.
*
* 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"): String {
return prefix + value.joinToString("") { encode(it) }
}
/**
* Converts the given ch into its integer representation considering it as an hexadecimal character.
*/
private fun hexToBin(ch: Char): Int = when (ch) {
in '0'..'9' -> ch - '0'
in 'A'..'F' -> ch - 'A' + 10
in 'a'..'f' -> ch - 'a' + 10
else -> throw(IllegalArgumentException("'$ch' is not a valid hex character"))
}
/**
* Parses the given value reading it as an hexadecimal string, and returns its byte array representation.
*
* Note that either 0x-prefixed string and no-prefixed hex strings are supported.
*
* @throws IllegalArgumentException if the value is not a hexadecimal string.
*/
fun decode(value: String): ByteArray {
// An hex string must always have length multiple of 2
if (value.length % 2 != 0) {
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
return ByteArray(cleanInput.length / 2).apply {
var i = 0
while (i < cleanInput.length) {
this[i / 2] = ((hexToBin(cleanInput[i]) shl 4) + hexToBin(cleanInput[i + 1])).toByte()
i += 2
}
}
}
}

View File

@ -0,0 +1,115 @@
/*
* MIT License
*
* Copyright (c) 2017 ligi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package dorkbox.bytes
/**
* Converts [this] [ByteArray] into its hexadecimal string representation prepending to it the given [prefix].
*
* 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, use the [toNoPrefixHexString] method or
* pass to this method an empty [prefix].
*/
public fun ByteArray.toHexString(prefix: String = "0x"): String = Hex.encode(this, prefix)
/**
* Converts [this] [ByteArray] into its hexadecimal representation without prepending any prefix to it.
*/
public fun ByteArray.toNoPrefixHexString(): String = toHexString(prefix = "")
/**
* Converts [this] [Collection] of bytes into its hexadecimal string representation prepending to it the given [prefix].
*
* 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, use the [toNoPrefixHexString] method or
* pass to this method an empty [prefix].
*/
public fun Collection<Byte>.toHexString(prefix: String = "0x"): String = Hex.encode(this.toByteArray(), prefix)
/**
* Converts [this] [Collection] of bytes into its hexadecimal representation without prepending any prefix to it.
*/
public fun Collection<Byte>.toNoPrefixHexString(): String = toHexString("")
/**
* Parses [this] [String] as an hexadecimal value and returns its [ByteArray] representation.
*
* Note that either 0x-prefixed string and no-prefixed hex strings are supported.
*
* @throws IllegalArgumentException if [this] is not a hexadecimal string.
*/
public fun HexString.hexToByteArray(): ByteArray = Hex.decode(string)
public fun String.hexToByteArray(): ByteArray = Hex.decode(this)
/**
* Returns `true` if and only if [this] value starts with the `0x` prefix.
*/
public fun HexString.has0xPrefix(): Boolean = string.startsWith("0x")
public fun String.has0xPrefix(): Boolean = this.startsWith("0x")
/**
* Returns a new [String] obtained by prepends the `0x` prefix to [this] value,
* only if it does not already have it.
*
* Examples:
* ```kotlin
* val myString = HexString("123")
* assertEquals("0x123", myString.prepend0xPrefix().string)
* assertEquals("0x0x123", myString.prepend0xPrefix().prepend0xPrefix().string)
* ```
*/
public fun HexString.prepend0xPrefix(): HexString = if (has0xPrefix()) this else HexString("0x$string")
public fun String.prepend0xPrefix(): String = if (has0xPrefix()) this else "0x$this"
/**
* Returns a new [String] obtained by removing the first occurrence of the `0x` prefix from [this] string, if it has it.
*
* Examples:
* ```kotlin
* assertEquals("123", HexString("123").clean0xPrefix().string)
* assertEquals("123", HexString("0x123").clean0xPrefix().string)
* assertEquals("0x123", HexString("0x0x123").clean0xPrefix().string)
* ```
*/
public fun HexString.clean0xPrefix(): HexString = if (has0xPrefix()) HexString(string.substring(2)) else this
public fun String.clean0xPrefix(): String = if (has0xPrefix()) this.substring(2) else this
/**
* Returns if a given string is a valid hex-string - either with or without 0x prefix
*/
public fun HexString.isValidHex(): Boolean = Hex.HEX_REGEX.matches(string)
public fun String.isValidHex(): Boolean = Hex.HEX_REGEX.matches(this)
/**
* Returns a HexString if a given string is a valid hex-string - either with or without 0x prefix
*/
public fun String.toHex(): HexString {
if (!this.isValidHex()) {
throw IllegalArgumentException("String is not hex")
}
return HexString(this)
}

View File

@ -0,0 +1,120 @@
/*
* Copyright 2021 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.bytes
import org.junit.Assert.*
import org.junit.Test
class TestHex {
private val hexRegex = Regex("0[xX][0-9a-fA-F]+")
@Test
fun weCanProduceSingleDigitHex() {
assertEquals(Hex.encode(0.toByte()), "00")
assertEquals(Hex.encode(1.toByte()), "01")
assertEquals(Hex.encode(15.toByte()), "0f")
}
@Test
fun weCanProduceDoubleDigitHex() {
assertEquals(Hex.encode(16.toByte()), "10")
assertEquals(Hex.encode(42.toByte()), "2a")
assertEquals(Hex.encode(255.toByte()), "ff")
}
@Test
fun prefixIsIgnored() {
assertTrue(Hex.decode("0xab").contentEquals(Hex.decode("ab")))
}
@Test
fun sizesAreOk() {
assertEquals(Hex.decode("0x").size, 0)
assertEquals(Hex.decode("ff").size, 1)
assertEquals(Hex.decode("ffaa").size, 2)
assertEquals(Hex.decode("ffaabb").size, 3)
assertEquals(Hex.decode("ffaabb44").size, 4)
assertEquals(Hex.decode("0xffaabb4455").size, 5)
assertEquals(Hex.decode("0xffaabb445566").size, 6)
assertEquals(Hex.decode("ffaabb44556677").size, 7)
}
@Test
fun exceptionOnOddInput() {
var exception: Exception? = null
try {
Hex.decode("0xa")
} catch (e: Exception) {
exception = e
}
assertTrue("Exception must be IllegalArgumentException", exception is IllegalArgumentException)
}
@Test
fun testRoundTrip() {
assertEquals(Hex.encode(Hex.decode("00")), "0x00")
assertEquals(Hex.encode(Hex.decode("ff")), "0xff")
assertEquals(Hex.encode(Hex.decode("abcdef")), "0xabcdef")
assertEquals(Hex.encode(Hex.decode("0xaa12456789bb")), "0xaa12456789bb")
}
@Test
fun regexMatchesForHEX() {
assertTrue(hexRegex.matches("0x00"))
assertTrue(hexRegex.matches("0xabcdef123456"))
}
@Test
fun regexFailsForNonHEX() {
assertFalse(hexRegex.matches("q"))
assertFalse(hexRegex.matches(""))
assertFalse(hexRegex.matches("0x+"))
assertFalse(hexRegex.matches("0xgg"))
}
@Test
fun detectsInvalidHex() {
var exception: Exception? = null
try {
Hex.decode("0xxx")
} catch (e: Exception) {
exception = e
}
assertTrue("Exception must be IllegalArgumentException", exception is IllegalArgumentException)
}
@Test
fun testHexString() {
val myString = HexString("123")
assertEquals("0x123", myString.prepend0xPrefix().string)
assertEquals("0x123", myString.prepend0xPrefix().prepend0xPrefix().string)
assertEquals("123", HexString("123").clean0xPrefix().string)
assertEquals("123", HexString("0x123").clean0xPrefix().string)
assertEquals("0x123", HexString("0x0x123").clean0xPrefix().string)
}
@Test
fun testStringAsHex() {
assertEquals("0x123", "123".prepend0xPrefix())
assertEquals("0x123", "123".prepend0xPrefix().prepend0xPrefix())
assertEquals("123", "123".clean0xPrefix())
assertEquals("123", "0x123".clean0xPrefix())
assertEquals("0x123", "0x0x123".clean0xPrefix())
}
}