diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt index 190eb3ff9..d0af6b68c 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/amethyst/benchmark/RichTextParserBenchmark.kt @@ -46,7 +46,7 @@ class RichTextParserBenchmark { fun parseApkUrl() { benchmarkRule.measureRepeated { assertNull( - RichTextParser().parseMediaUrl( + RichTextParser().createMediaContent( "https://github.com/vitorpamplona/amethyst/releases/download/v0.83.10/amethyst-googleplay-universal-v0.83.10.apk", EmptyTagList, null, diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt index b18c944c2..68266efd8 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt @@ -24,7 +24,6 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.encoders.HexValidator -import junit.framework.TestCase.assertEquals import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -37,58 +36,59 @@ import org.junit.runner.RunWith */ @RunWith(AndroidJUnit4::class) class HexBenchmark { - @get:Rule val benchmarkRule = BenchmarkRule() + @get:Rule val r = BenchmarkRule() - val testHex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf" + val hex = "48a72b485d38338627ec9d427583551f9af4f016c739b8ec0d6313540a8b12cf" + val bytes = + fr.acinq.secp256k1.Hex + .decode(hex) @Test fun hexDecodeOurs() { - benchmarkRule.measureRepeated { + r.measureRepeated { com.vitorpamplona.quartz.encoders.Hex - .decode(testHex) + .decode(hex) } } @Test fun hexEncodeOurs() { - val bytes = + r.measureRepeated { com.vitorpamplona.quartz.encoders.Hex - .decode(testHex) - - benchmarkRule.measureRepeated { - assertEquals( - testHex, - com.vitorpamplona.quartz.encoders.Hex - .encode(bytes), - ) + .encode(bytes) } } @Test fun hexDecodeBaseSecp() { - benchmarkRule.measureRepeated { + r.measureRepeated { fr.acinq.secp256k1.Hex - .decode(testHex) + .decode(hex) } } @Test fun hexEncodeBaseSecp() { - val bytes = + r.measureRepeated { fr.acinq.secp256k1.Hex - .decode(testHex) - - benchmarkRule.measureRepeated { - assertEquals( - testHex, - fr.acinq.secp256k1.Hex - .encode(bytes), - ) + .encode(bytes) } } + @OptIn(ExperimentalStdlibApi::class) + @Test + fun hexDecodeKotlin() { + r.measureRepeated { hex.hexToByteArray(HexFormat.Default) } + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun hexEncodeKotlin() { + r.measureRepeated { bytes.toHexString(HexFormat.Default) } + } + @Test fun isHex() { - benchmarkRule.measureRepeated { HexValidator.isHex(testHex) } + r.measureRepeated { HexValidator.isHex(hex) } } } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/encoders/HexEncodingTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/encoders/HexEncodingTest.kt index 485444251..9f0185310 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/encoders/HexEncodingTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/encoders/HexEncodingTest.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.encoders import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.crypto.CryptoUtils import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -69,4 +71,43 @@ class HexEncodingTest { ) } } + + @Test + fun testIsHex() { + assertFalse("/0", HexValidator.isHex("/0")) + assertFalse("/.", HexValidator.isHex("/.")) + assertFalse("!!", HexValidator.isHex("!!")) + assertFalse("::", HexValidator.isHex("::")) + assertFalse("@@", HexValidator.isHex("@@")) + assertFalse("GG", HexValidator.isHex("GG")) + assertFalse("FG", HexValidator.isHex("FG")) + assertFalse("`a", HexValidator.isHex("`a")) + assertFalse("gg", HexValidator.isHex("gg")) + assertFalse("fg", HexValidator.isHex("fg")) + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun testRandomsIsHex() { + for (i in 0..10000) { + val bytes = CryptoUtils.privkeyCreate() + val hex = bytes.toHexString(HexFormat.Default) + assertTrue(hex, HexValidator.isHex(hex)) + val hexUpper = bytes.toHexString(HexFormat.UpperCase) + assertTrue(hexUpper, HexValidator.isHex(hexUpper)) + } + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun testRandomsUppercase() { + for (i in 0..1000) { + val bytes = CryptoUtils.privkeyCreate() + val hex = bytes.toHexString(HexFormat.UpperCase) + assertEquals( + bytes.toList(), + Hex.decode(hex).toList(), + ) + } + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt index 092cba20a..9f87a658d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/encoders/HexUtils.kt @@ -27,65 +27,53 @@ fun ByteArray.toHexKey(): HexKey = Hex.encode(this) fun HexKey.hexToByteArray(): ByteArray = Hex.decode(this) -object HexValidator { - private fun isHexChar(c: Char): Boolean = - when (c) { - in '0'..'9' -> true - in 'a'..'f' -> true - in 'A'..'F' -> true - else -> false - } +val lowerCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') +val upperCaseHex = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') +val hexToByte: IntArray = + IntArray(256) { -1 }.apply { + lowerCaseHex.forEachIndexed { index, char -> this[char.code] = index } + upperCaseHex.forEachIndexed { index, char -> this[char.code] = index } + } + +// Encodes both chars in a single Int variable +val byteToHex = + IntArray(256) { + (lowerCaseHex[(it shr 4)].code shl 8) or lowerCaseHex[(it and 0xF)].code + } + +object HexValidator { fun isHex(hex: String?): Boolean { if (hex == null) return false if (hex.isEmpty()) return false - if (hex.length % 2 != 0) return false // must be even - var isHex = true + if (hex.length and 1 != 0) return false // must be even - for (c in hex) { - if (!isHexChar(c)) { - isHex = false - break - } + for (c in hex.indices) { + if (hexToByte[hex[c].code] < 0) return false } - return isHex + + return true } } object Hex { - val hexCode = - arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') - - // Faster if no calculations are needed. - 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("illegal hex character: $ch") - } - @JvmStatic fun decode(hex: String): ByteArray { // faster version of hex decoder - require(hex.length % 2 == 0) - val outSize = hex.length / 2 - val out = ByteArray(outSize) - - for (i in 0 until outSize) { - out[i] = (hexToBin(hex[2 * i]) * 16 + hexToBin(hex[2 * i + 1])).toByte() + require(hex.length and 1 == 0) + return ByteArray(hex.length / 2) { + (hexToByte[hex[2 * it].code] shl 4 or hexToByte[hex[2 * it + 1].code]).toByte() } - - return out } @JvmStatic fun encode(input: ByteArray): String { - val len = input.size - val out = CharArray(len * 2) - for (i in 0 until len) { - out[i * 2] = hexCode[(input[i].toInt() shr 4) and 0xF] - out[i * 2 + 1] = hexCode[input[i].toInt() and 0xF] + val out = CharArray(input.size * 2) + var outIdx = 0 + for (i in 0 until input.size) { + val chars = byteToHex[input[i].toInt() and 0xFF] + out[outIdx++] = (chars shr 8).toChar() + out[outIdx++] = (chars and 0xFF).toChar() } return String(out) }