All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dorkbox.hex.HexExtensions.kt Maven / Gradle / Ivy

/*
 * Copyright 2023 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.hex

import dorkbox.bytes.BigEndian
import dorkbox.hex.Hex.tweakHex
import java.math.BigInteger

object Hex {
    /**
     * Gets the version number.
     */
    const val version = "1.0"

    init {
        // Add this project to the updates system, which verifies this class + UUID + version information
        dorkbox.updates.Updates.add(Hex::class.java, "e52265b05ab54361a011ca8d406b9db9", version)
    }

    /**
     * Represents all the chars used for nibble
     */
    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]+")
    private val prefix = "0x".toCharArray()

    /**
     * Encodes the given byte value as a hexadecimal character.
     */
    fun encode(value: Byte): String {
        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 {
        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)
    }

    /**
     * Encodes the given byte array value to its hexadecimal representations, and optionally prepends the prefix `0x` to it.
     */
    fun encode(bytes: ByteArray, usePrefix: Boolean = true, 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
        }

        var j: Int
        val hexString = if (usePrefix) {
            j = 2
            val array = CharArray(j + ((2 * (length - start) )))
            prefix.copyInto(array)
            array
        } else {
            j = 0
            CharArray(((2 * (length - start) )))
        }


        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 a hexadecimal character.
     */
    private fun hexToDecimal(char: Char): Int = when (char) {
        in '0'..'9' -> char - '0'
        in 'A'..'F' -> char - 'A' + 10
        in 'a'..'f' -> char - 'a' + 10
        else -> throw(IllegalArgumentException("'$char' is not a valid hexadecimal character"))
    }

    /**
     * Parses the given value reading it as an hexadecimal string, and returns a hex bytearray
     *
     * Note that either `0x`, `0X`, and no-prefixed strings are supported.
     *
     * @throws IllegalArgumentException if the value is not a hexadecimal string.
     */
    fun decode(value: CharSequence): ByteArray {
        // Remove the 0x or 0X prefix if it is set
        val cleanInput = value.removeHexPrefix()

        if (cleanInput.isEmpty()) {
            return ByteArray(0)
        }

        // A hex string must always have length multiple of 2
        if (cleanInput.length % 2 != 0) {
            throw IllegalArgumentException("hex-string '$value' must have an even number of digits")
        }

        return ByteArray(cleanInput.length / 2).apply {
            var i = 0
            while (i < cleanInput.length) {
                this[i / 2] = ((hexToDecimal(cleanInput[i]) shl 4) + hexToDecimal(cleanInput[i + 1])).toByte()
                i += 2
            }
        }
    }

    internal fun String.tweakHex(value: Int, usePrefix: Boolean = true, toUpperCase: Boolean = false): String {
        val prefix = if (usePrefix) {
            if (-10 < value && value < 10) {
                "0x0"
            } else {
                "0x"
            }
        } else {
            if (-10 < value && value < 10) {
                "0"
            } else {
                ""
            }
        }

        return if (toUpperCase) {
            prefix + this.uppercase()
        } else {
            "$prefix$this"
        }
    }

    internal fun String.tweakHex(value: Long, usePrefix: Boolean = true, toUpperCase: Boolean = false): String {
        val prefix = if (usePrefix) {
            if (-10 < value && value < 10) {
                "0x0"
            } else {
                "0x"
            }
        } else {
            if (-10 < value && value < 10) {
                "0"

            } else {
                ""
            }
        }

        return if (toUpperCase) {
            prefix + this.uppercase()
        } else {
            "$prefix$this"
        }
    }

    private val bigInt10 = BigInteger.valueOf(10)
    private val bigIntN10 = BigInteger.valueOf(-10)

    internal fun BigInteger.tweakHex(usePrefix: Boolean = true, toUpperCase: Boolean = false): String {
        val asHex = this.toString(16)

        val prefix = if (usePrefix) {
            if (bigIntN10 < this && this < bigInt10) {
                "0x0"
            } else {
                "0x"
            }
        } else {
            if (bigIntN10 > this && this < bigInt10) {
                "0"

            } else {
                ""
            }
        }

        return if (toUpperCase) {
            prefix + asHex.uppercase()
        } else {
            "$prefix$asHex"
        }
    }
}

/**
 * Converts a [ByteArray] into its hexadecimal string representation, optionally with the `0x` prefix.
 */
fun ByteArray.toHexString(usePrefix: Boolean = true, start: Int = 0, length: Int = this.size, toUpperCase: Boolean = false): String =
    Hex.encode(this, usePrefix, start, length, toUpperCase)

/**
 * Converts a [Collection] into its hexadecimal string representation, optionally with the `0x` prefix.
 */
fun Collection.toHexString(usePrefix: Boolean = true, length: Int = this.size): String = Hex.encode(this.toByteArray(), usePrefix, length)


/**
 * Parses the [String] as an hexadecimal value and returns its [ByteArray] representation.
 *
 * Note that both `0x`, `0X`, and no-prefix hex strings are supported.
 *
 * @throws IllegalArgumentException if [this] is not a hexadecimal string.
 */
fun CharSequence.hexToByteArray(): ByteArray = Hex.decode(this)



/**
 * Parses the [String] as an hexadecimal value and returns its [Byte] representation.
 *
 * Note that both `0x`, `0X`, and no-prefix hex strings are supported.
 *
 * @throws IllegalArgumentException if [this] is not a hexadecimal string.
 */
fun CharSequence.hexToByte(): Byte = Hex.decode(this).first()

/**
 * Parses the [String] as an hexadecimal value and returns its [UByte] representation.
 *
 * Note that both `0x`, `0X`, and no-prefix hex strings are supported.
 *
 * @throws IllegalArgumentException if [this] is not a hexadecimal string.
 */
fun CharSequence.hexToUByte(): UByte = Hex.decode(this).first().toUByte()

/**
 * Parses the [String] as an hexadecimal value and returns its [Short] representation.
 *
 * Note that both `0x`, `0X`, and no-prefix hex strings are supported.
 *
 * @throws IllegalArgumentException if [this] is not a hexadecimal string.
 */
fun CharSequence.hexToShort(): Short = BigEndian.Int_.from(Hex.decode(this)).toShort()

/**
 * Parses the [String] as an hexadecimal value and returns its [UShort] representation.
 *
 * Note that both `0x`, `0X`, and no-prefix hex strings are supported.
 *
 * @throws IllegalArgumentException if [this] is not a hexadecimal string.
 */
fun CharSequence.hexToUShort(): UShort = BigEndian.UShort_.from(Hex.decode(this))

/**
 * Parses the [String] as an hexadecimal value and returns its [Int] representation.
 *
 * Note that both `0x`, `0X`, and no-prefix hex strings are supported.
 *
 * @throws IllegalArgumentException if [this] is not a hexadecimal string.
 */
fun CharSequence.hexToInt(): Int = BigEndian.Int_.from(Hex.decode(this))

/**
 * Parses the [String] as an hexadecimal value and returns its [UShort] representation.
 *
 * Note that both `0x`, `0X`, and no-prefix hex strings are supported.
 *
 * @throws IllegalArgumentException if [this] is not a hexadecimal string.
 */
fun CharSequence.hexToUInt(): UInt = BigEndian.UInt_.from(Hex.decode(this))

/**
 * Parses the [String] as an hexadecimal value and returns its [Long] representation.
 *
 * Note that both `0x`, `0X`, and no-prefix hex strings are supported.
 *
 * @throws IllegalArgumentException if [this] is not a hexadecimal string.
 */
fun CharSequence.hexToLong(): Long = BigEndian.Long_.from(Hex.decode(this))

/**
 * Parses the [String] as an hexadecimal value and returns its [ULong] representation.
 *
 * Note that both `0x`, `0X`, and no-prefix hex strings are supported.
 *
 * @throws IllegalArgumentException if [this] is not a hexadecimal string.
 */
fun CharSequence.hexToULong(): ULong = BigEndian.ULong_.from(Hex.decode(this))





/**
 * Returns `true` if and only if [this] value starts with the `0x` or `0X` prefix.
 */
fun CharSequence.hasPrefix(): Boolean = this.startsWith("0x") || this.startsWith("0X")

/**
 * Returns a new [String] obtained by prepends the `0x` prefix to the string, but only if it not already present.
 *
 * Examples:
 * ```kotlin
 * assertEquals("0x123", "123".addHexPrefix())
 * assertEquals("0x123", "123".addHexPrefix().addHexPrefix())
 * ```
 */
fun String.addHexPrefix(): String = if (hasPrefix()) this else "0x$this"
fun CharSequence.addHexPrefix(): CharSequence = if (hasPrefix()) this else "0x$this"

/**
 * Returns a new [String] obtained by removing the first occurrence of the `0x` prefix from the string, if it has it.
 *
 * Examples:
 * ```kotlin
 * assertEquals("123", "123".removeHexPrefix())
 * assertEquals("123", "0x123".removeHexPrefix())
 * assertEquals("0x123", "0x0x123".removeHexPrefix())
 * ```
 */
fun String.removeHexPrefix(): String = if (hasPrefix()) this.substring(2) else this
fun CharSequence.removeHexPrefix(): CharSequence = if (hasPrefix()) this.substring(2) else this

/**
 * Returns if a given string is a valid hex-string - either with or without 0x prefix
 */
fun CharSequence.isValidHex(): Boolean = Hex.HEX_REGEX.matches(this)

fun Byte.toHexString(usePrefix: Boolean = true, toUpperCase: Boolean = false): String {
    return if (toUpperCase) {
        if (usePrefix) {
            "0x" + Hex.encodeUpper(this)
        } else {
            Hex.encodeUpper(this)
        }

    } else {
        if (usePrefix) {
            "0x" + Hex.encode(this)
        } else {
            Hex.encode(this)
        }
    }
}

fun UByte.toHexString(usePrefix: Boolean = true, toUpperCase: Boolean = false): String = Integer.toHexString(this.toInt()).tweakHex(this.toInt(), usePrefix, toUpperCase)
fun Short.toHexString(usePrefix: Boolean = true, toUpperCase: Boolean = false): String = Integer.toHexString(this.toInt()).tweakHex(this.toInt(), usePrefix, toUpperCase)
fun UShort.toHexString(usePrefix: Boolean = true, toUpperCase: Boolean = false): String = Integer.toHexString(this.toInt()).tweakHex(this.toInt(), usePrefix, toUpperCase)
fun Int.toHexString(usePrefix: Boolean = true, upperCase: Boolean = false): String = Integer.toHexString(this).tweakHex(this, usePrefix, upperCase)
fun UInt.toHexString(usePrefix: Boolean = true, toUpperCase: Boolean = false): String = java.lang.Long.toHexString(this.toLong()).tweakHex(this.toLong(), usePrefix, toUpperCase)
fun Long.toHexString(usePrefix: Boolean = true, toUpperCase: Boolean = false): String = java.lang.Long.toHexString(this).tweakHex(this, usePrefix, toUpperCase)
fun ULong.toHexString(usePrefix: Boolean = true, toUpperCase: Boolean = false): String = BigInteger(this.toString()).tweakHex(usePrefix, toUpperCase)


fun ByteArray.hexDump(fromIndex: Int = 0, length: Int = this.size): String = HexUtil.hexDump(this, fromIndex, length)
fun ByteArray.prettyHexDump(fromIndex: Int = 0, length: Int = this.size): String = HexUtil.prettyHexDump(this, fromIndex, length)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy