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 e4553ade8..f603471a7 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt @@ -100,17 +100,7 @@ class HexBenchmark { } @Test - fun newIsHex() { - val isHexChar = - BooleanArray(256).apply { - "0123456789abcdefABCDEF".forEach { this[it.code] = true } - } - - r.measureRepeated { - for (c in hex.indices) { - if (!isHexChar[hex[c].code]) return@measureRepeated - } - return@measureRepeated - } + fun isHex64() { + r.measureRepeated { Hex.isHex64(hex) } } } diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/utils/HexEncodingTest.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/utils/HexEncodingTest.kt index 2eab43448..c53f67157 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/utils/HexEncodingTest.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/utils/HexEncodingTest.kt @@ -83,17 +83,50 @@ class HexEncodingTest { assertFalse("`a", Hex.isHex("`a")) assertFalse("gg", Hex.isHex("gg")) assertFalse("fg", Hex.isHex("fg")) + assertFalse("\uD83E\uDD70", Hex.isHex("\uD83E\uDD70")) } @OptIn(ExperimentalStdlibApi::class) @Test fun testRandomsIsHex() { + val lowerCaseHexNeg = "ghijklmnopqrstuvwxyz" + val upperCaseHexNeg = "GHIJKLMNOPQRSTUVWXYZ" + for (i in 0..10000) { val bytes = RandomInstance.bytes(32) val hex = bytes.toHexString(HexFormat.Default) assertTrue(hex, Hex.isHex(hex)) val hexUpper = bytes.toHexString(HexFormat.UpperCase) assertTrue(hexUpper, Hex.isHex(hexUpper)) + + // scramble + val negHex = hex.replaceFirst(hex.random(), lowerCaseHexNeg.random()) + val negHexUpper = hexUpper.replaceFirst(hexUpper.random(), upperCaseHexNeg.random()) + + assertFalse(negHex, Hex.isHex(negHex)) + assertFalse(negHexUpper, Hex.isHex(negHexUpper)) + } + } + + @OptIn(ExperimentalStdlibApi::class) + @Test + fun testRandomsIsHex64() { + val lowerCaseHexNeg = "ghijklmnopqrstuvwxyz" + val upperCaseHexNeg = "GHIJKLMNOPQRSTUVWXYZ" + + for (i in 0..10000) { + val bytes = RandomInstance.bytes(32) + val hex = bytes.toHexString(HexFormat.Default) + assertTrue(hex, Hex.isHex64(hex)) + val hexUpper = bytes.toHexString(HexFormat.UpperCase) + assertTrue(hexUpper, Hex.isHex64(hexUpper)) + + // scramble + val negHex = hex.replaceFirst(hex.random(), lowerCaseHexNeg.random()) + val negHexUpper = hexUpper.replaceFirst(hexUpper.random(), upperCaseHexNeg.random()) + + assertFalse(hex + ":" + negHex, Hex.isHex64(negHex)) + assertFalse(hexUpper + ":" + negHexUpper, Hex.isHex64(negHexUpper)) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt index 4882f15fc..b42bd9f4b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt @@ -36,23 +36,114 @@ object Hex { (LOWER_CASE_HEX[(it shr 4)].code shl 8) or LOWER_CASE_HEX[(it and 0xF)].code } + // 47ns in debug on the Emulator fun isHex(hex: String?): Boolean { - if (hex.isNullOrEmpty()) return false + if (hex == null) return false if (hex.length and 1 != 0) return false - try { - for (c in hex.indices) { - if (c < 0 || c > 255) return false - if (hexToByte[hex[c].code] < 0) return false - } + return try { + internalIsHex(hex, hexToByte) } catch (_: IllegalArgumentException) { // there are p tags with emoji's which makes the hex[c].code > 256 - return false + false + } catch (_: IndexOutOfBoundsException) { + // there are p tags with emoji's which makes the hex[c].code > 256 + false } + } + // breaking this function away from the main one improves performance for some reason + fun internalIsHex( + hex: String, + hexToByte: IntArray, + ): Boolean { + for (c in hex.indices) { + if (hexToByte[hex[c].code] < 0) return false + } return true } + // 30% faster than isHex + fun isHex64(hex: String): Boolean = + try { + hexToByte[hex[0].code] >= 0 && + hexToByte[hex[1].code] >= 0 && + hexToByte[hex[2].code] >= 0 && + hexToByte[hex[3].code] >= 0 && + hexToByte[hex[4].code] >= 0 && + hexToByte[hex[5].code] >= 0 && + hexToByte[hex[6].code] >= 0 && + hexToByte[hex[7].code] >= 0 && + hexToByte[hex[8].code] >= 0 && + hexToByte[hex[9].code] >= 0 && + + hexToByte[hex[10].code] >= 0 && + hexToByte[hex[11].code] >= 0 && + hexToByte[hex[12].code] >= 0 && + hexToByte[hex[13].code] >= 0 && + hexToByte[hex[14].code] >= 0 && + hexToByte[hex[15].code] >= 0 && + hexToByte[hex[16].code] >= 0 && + hexToByte[hex[17].code] >= 0 && + hexToByte[hex[18].code] >= 0 && + hexToByte[hex[19].code] >= 0 && + + hexToByte[hex[20].code] >= 0 && + hexToByte[hex[21].code] >= 0 && + hexToByte[hex[22].code] >= 0 && + hexToByte[hex[23].code] >= 0 && + hexToByte[hex[24].code] >= 0 && + hexToByte[hex[25].code] >= 0 && + hexToByte[hex[26].code] >= 0 && + hexToByte[hex[27].code] >= 0 && + hexToByte[hex[28].code] >= 0 && + hexToByte[hex[29].code] >= 0 && + + hexToByte[hex[30].code] >= 0 && + hexToByte[hex[31].code] >= 0 && + hexToByte[hex[32].code] >= 0 && + hexToByte[hex[33].code] >= 0 && + hexToByte[hex[34].code] >= 0 && + hexToByte[hex[35].code] >= 0 && + hexToByte[hex[36].code] >= 0 && + hexToByte[hex[37].code] >= 0 && + hexToByte[hex[38].code] >= 0 && + hexToByte[hex[39].code] >= 0 && + + hexToByte[hex[40].code] >= 0 && + hexToByte[hex[41].code] >= 0 && + hexToByte[hex[42].code] >= 0 && + hexToByte[hex[43].code] >= 0 && + hexToByte[hex[44].code] >= 0 && + hexToByte[hex[45].code] >= 0 && + hexToByte[hex[46].code] >= 0 && + hexToByte[hex[47].code] >= 0 && + hexToByte[hex[48].code] >= 0 && + hexToByte[hex[49].code] >= 0 && + + hexToByte[hex[50].code] >= 0 && + hexToByte[hex[51].code] >= 0 && + hexToByte[hex[52].code] >= 0 && + hexToByte[hex[53].code] >= 0 && + hexToByte[hex[54].code] >= 0 && + hexToByte[hex[55].code] >= 0 && + hexToByte[hex[56].code] >= 0 && + hexToByte[hex[57].code] >= 0 && + hexToByte[hex[58].code] >= 0 && + hexToByte[hex[59].code] >= 0 && + + hexToByte[hex[60].code] >= 0 && + hexToByte[hex[61].code] >= 0 && + hexToByte[hex[62].code] >= 0 && + hexToByte[hex[63].code] >= 0 + } catch (_: IllegalArgumentException) { + // there are p tags with emoji's which makes the hex[c].code > 256 + false + } catch (_: IndexOutOfBoundsException) { + // there are p tags with emoji's which makes the hex[c].code > 256 + false + } + fun decode(hex: String): ByteArray { // faster version of hex decoder require(hex.length and 1 == 0)