From 55faf1d670045e1fee1198dc3505073bc79cbb27 Mon Sep 17 00:00:00 2001 From: Infendro Date: Sat, 17 Jan 2026 12:27:41 +0100 Subject: [PATCH] implement numeric encodings, optimize --- .../kotlin/com/infendro/encoding/Base16.kt | 16 ---- .../kotlin/com/infendro/encoding/Base2.kt | 15 --- .../kotlin/com/infendro/encoding/Base32.kt | 19 ---- .../kotlin/com/infendro/encoding/Base64.kt | 23 ----- .../kotlin/com/infendro/encoding/Base8.kt | 15 --- .../kotlin/com/infendro/encoding/Encoding.kt | 95 ++---------------- .../com/infendro/encoding/numeric/Base36.kt | 5 + .../com/infendro/encoding/numeric/Base62.kt | 5 + .../encoding/numeric/NumericEncoding.kt | 40 ++++++++ .../com/infendro/encoding/stream/Base16.kt | 5 + .../com/infendro/encoding/stream/Base2.kt | 5 + .../com/infendro/encoding/stream/Base32.kt | 6 ++ .../com/infendro/encoding/stream/Base64.kt | 6 ++ .../com/infendro/encoding/stream/Base8.kt | 5 + .../encoding/stream/StreamEncoding.kt | 96 +++++++++++++++++++ .../com/infendro/encoding/util/BigInt.kt | 89 +++++++++++++++++ .../kotlin/com/infendro/encoding/util/Math.kt | 21 ++-- .../kotlin/com/infendro/encoding/Base16.kt | 24 ++--- .../kotlin/com/infendro/encoding/Base2.kt | 42 ++++---- .../kotlin/com/infendro/encoding/Base32.kt | 24 ++--- .../kotlin/com/infendro/encoding/Base64.kt | 22 +++-- .../kotlin/com/infendro/encoding/Base8.kt | 26 ++--- 22 files changed, 355 insertions(+), 249 deletions(-) delete mode 100644 src/commonMain/kotlin/com/infendro/encoding/Base16.kt delete mode 100644 src/commonMain/kotlin/com/infendro/encoding/Base2.kt delete mode 100644 src/commonMain/kotlin/com/infendro/encoding/Base32.kt delete mode 100644 src/commonMain/kotlin/com/infendro/encoding/Base64.kt delete mode 100644 src/commonMain/kotlin/com/infendro/encoding/Base8.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/numeric/Base36.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/numeric/Base62.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/numeric/NumericEncoding.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/stream/Base16.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/stream/Base2.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/stream/Base32.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/stream/Base64.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/stream/Base8.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/stream/StreamEncoding.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/util/BigInt.kt diff --git a/src/commonMain/kotlin/com/infendro/encoding/Base16.kt b/src/commonMain/kotlin/com/infendro/encoding/Base16.kt deleted file mode 100644 index 4c23230..0000000 --- a/src/commonMain/kotlin/com/infendro/encoding/Base16.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.infendro.encoding - -object Base16 : Encoding() { - private val CHARACTERS = ubyteArrayOf( - 0x30U, 0x31U, 0x32U, 0x33U, 0x34U, 0x35U, 0x36U, 0x37U, - 0x38U, 0x39U, 0x61U, 0x62U, 0x63U, 0x64U, 0x65U, 0x66U, - ) - - override fun encode( - bytes: UByteArray, - ) = encode(bytes, CHARACTERS) - - override fun decode( - bytes: UByteArray, - ) = decode(bytes, CHARACTERS) -} diff --git a/src/commonMain/kotlin/com/infendro/encoding/Base2.kt b/src/commonMain/kotlin/com/infendro/encoding/Base2.kt deleted file mode 100644 index a6de71b..0000000 --- a/src/commonMain/kotlin/com/infendro/encoding/Base2.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.infendro.encoding - -object Base2 : Encoding() { - private val CHARACTERS = ubyteArrayOf( - 0x30U, 0x31U, - ) - - override fun encode( - bytes: UByteArray, - ) = encode(bytes, CHARACTERS) - - override fun decode( - bytes: UByteArray, - ) = decode(bytes, CHARACTERS) -} diff --git a/src/commonMain/kotlin/com/infendro/encoding/Base32.kt b/src/commonMain/kotlin/com/infendro/encoding/Base32.kt deleted file mode 100644 index 007471f..0000000 --- a/src/commonMain/kotlin/com/infendro/encoding/Base32.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.infendro.encoding - -object Base32 : Encoding() { - private val CHARACTERS = ubyteArrayOf( - 0x41U, 0x42U, 0x43U, 0x44U, 0x45U, 0x46U, 0x47U, 0x48U, - 0x49U, 0x4aU, 0x4bU, 0x4cU, 0x4dU, 0x4eU, 0x4fU, 0x50U, - 0x51U, 0x52U, 0x53U, 0x54U, 0x55U, 0x56U, 0x57U, 0x58U, - 0x59U, 0x5aU, 0x32U, 0x33U, 0x34U, 0x35U, 0x36U, 0x37U, - ) - private const val PADDING: UByte = 0x3dU - - override fun encode( - bytes: UByteArray, - ) = encode(bytes, CHARACTERS, PADDING) - - override fun decode( - bytes: UByteArray, - ) = decode(bytes, CHARACTERS, PADDING) -} diff --git a/src/commonMain/kotlin/com/infendro/encoding/Base64.kt b/src/commonMain/kotlin/com/infendro/encoding/Base64.kt deleted file mode 100644 index bcf1c6c..0000000 --- a/src/commonMain/kotlin/com/infendro/encoding/Base64.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.infendro.encoding - -object Base64 : Encoding() { - private val CHARACTERS = ubyteArrayOf( - 0x41U, 0x42U, 0x43U, 0x44U, 0x45U, 0x46U, 0x47U, 0x48U, - 0x49U, 0x4aU, 0x4bU, 0x4cU, 0x4dU, 0x4eU, 0x4fU, 0x50U, - 0x51U, 0x52U, 0x53U, 0x54U, 0x55U, 0x56U, 0x57U, 0x58U, - 0x59U, 0x5aU, 0x61U, 0x62U, 0x63U, 0x64U, 0x65U, 0x66U, - 0x67U, 0x68U, 0x69U, 0x6aU, 0x6bU, 0x6cU, 0x6dU, 0x6eU, - 0x6fU, 0x70U, 0x71U, 0x72U, 0x73U, 0x74U, 0x75U, 0x76U, - 0x77U, 0x78U, 0x79U, 0x7aU, 0x30U, 0x31U, 0x32U, 0x33U, - 0x34U, 0x35U, 0x36U, 0x37U, 0x38U, 0x39U, 0x2bU, 0x2fU, - ) - private const val PADDING: UByte = 0x3dU - - override fun encode( - bytes: UByteArray, - ) = encode(bytes, CHARACTERS, PADDING) - - override fun decode( - bytes: UByteArray, - ) = decode(bytes, CHARACTERS, PADDING) -} diff --git a/src/commonMain/kotlin/com/infendro/encoding/Base8.kt b/src/commonMain/kotlin/com/infendro/encoding/Base8.kt deleted file mode 100644 index ac9dccc..0000000 --- a/src/commonMain/kotlin/com/infendro/encoding/Base8.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.infendro.encoding - -object Base8 : Encoding() { - private val CHARACTERS = ubyteArrayOf( - 0x30U, 0x31U, 0x32U, 0x33U, 0x34U, 0x35U, 0x36U, 0x37U, - ) - - override fun encode( - bytes: UByteArray, - ) = encode(bytes, CHARACTERS) - - override fun decode( - bytes: UByteArray, - ) = decode(bytes, CHARACTERS) -} diff --git a/src/commonMain/kotlin/com/infendro/encoding/Encoding.kt b/src/commonMain/kotlin/com/infendro/encoding/Encoding.kt index 211a271..3d1b456 100644 --- a/src/commonMain/kotlin/com/infendro/encoding/Encoding.kt +++ b/src/commonMain/kotlin/com/infendro/encoding/Encoding.kt @@ -1,92 +1,15 @@ package com.infendro.encoding -import com.infendro.encoding.util.lcm -import com.infendro.encoding.util.log2 -import kotlin.math.pow - -sealed class Encoding { - abstract fun encode( - bytes: UByteArray, - ): UByteArray - - abstract fun decode( - bytes: UByteArray, - ): UByteArray - - protected fun encode( - bytes: UByteArray, - characters: UByteArray, - padding: UByte? = null, - ): UByteArray { - val length = log2(characters.size).toInt() - val mask = (2.0.pow(length) - 1).toUInt() - - val encoded = mutableListOf() - - var buffer = 0U - var bits = 0 - - for (byte in bytes) { - buffer = (buffer shl 8) + byte - bits += 8 - - while (bits >= length) { - val offset = bits - length - val index = (buffer shr offset).toInt() - buffer = buffer and (mask shl offset).inv() - bits -= length - - encoded.add(characters[index]) - } - } - if (bits > 0) { - buffer = buffer shl (length - bits) - val index = buffer.toInt() - encoded.add(characters[index]) - } - - if (padding != null) { - val block = lcm(length, 8) / length - while (encoded.size % block != 0) { - encoded.add(padding) - } - } - - return encoded.toUByteArray() +abstract class Encoding( + val alphabet: ByteArray, +) { + init { + require(alphabet.isNotEmpty()) } - protected fun decode( - bytes: UByteArray, - characters: UByteArray, - padding: UByte? = null, - ): UByteArray { - val length = log2(characters.size).toInt() - val mask = 0xffU + val base: Int + get() = alphabet.size - val decoded = mutableListOf() - - var buffer = 0U - var bits = 0 - - for (byte in bytes) { - if (byte == padding) break - - val index = characters.indexOf(byte) - if (index == -1) throw Exception() - - buffer = (buffer shl length) + index.toUByte() - bits += length - - if (bits >= 8) { - val offset = bits - 8 - val character = (buffer shr offset).toUByte() - buffer = buffer and (mask shl offset).inv() - bits -= 8 - - decoded.add(character) - } - } - - return decoded.toUByteArray() - } + abstract fun encode(bytes: ByteArray): ByteArray + abstract fun decode(bytes: ByteArray): ByteArray } diff --git a/src/commonMain/kotlin/com/infendro/encoding/numeric/Base36.kt b/src/commonMain/kotlin/com/infendro/encoding/numeric/Base36.kt new file mode 100644 index 0000000..dd1a268 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/encoding/numeric/Base36.kt @@ -0,0 +1,5 @@ +package com.infendro.encoding.numeric + +private val alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".encodeToByteArray() + +object Base36 : NumericEncoding(alphabet) diff --git a/src/commonMain/kotlin/com/infendro/encoding/numeric/Base62.kt b/src/commonMain/kotlin/com/infendro/encoding/numeric/Base62.kt new file mode 100644 index 0000000..d8e0dac --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/encoding/numeric/Base62.kt @@ -0,0 +1,5 @@ +package com.infendro.encoding.numeric + +private val alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".encodeToByteArray() + +object Base62 : NumericEncoding(alphabet) diff --git a/src/commonMain/kotlin/com/infendro/encoding/numeric/NumericEncoding.kt b/src/commonMain/kotlin/com/infendro/encoding/numeric/NumericEncoding.kt new file mode 100644 index 0000000..16b36d4 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/encoding/numeric/NumericEncoding.kt @@ -0,0 +1,40 @@ +package com.infendro.encoding.numeric + +import com.infendro.encoding.Encoding +import com.infendro.encoding.util.BigInt +import com.infendro.encoding.util.log2 +import kotlin.math.ceil + +open class NumericEncoding( + alphabet: ByteArray, +) : Encoding(alphabet) { + override fun encode(bytes: ByteArray): ByteArray { + if (bytes.isEmpty()) return byteArrayOf() + + val size = ceil(bytes.size * 8 / log2(base)).toInt() + val result = ByteArray(size) + + var x = BigInt.from(bytes) + var i = result.size + while (!x.isZero()) { + val (quotient, remainder) = x.divRem(base) + x = quotient + result[--i] = alphabet[remainder] + } + + return result.copyOfRange(i, result.size) + } + + override fun decode(bytes: ByteArray): ByteArray { + if (bytes.isEmpty()) return byteArrayOf() + + var value = BigInt.zero + for (i in 0.. size + else -> { + val block = lcm(bits, 8) / bits + size.align(block) + } + } + val result = ByteArray(encodedSize) + + var r = 0 + var buffer = 0 + var bufferBits = 0 + + for (i in bytes.indices) { + buffer = (buffer shl 8) + bytes[i] + bufferBits += 8 + + while (bufferBits >= bits) { + val offset = bufferBits - bits + val index = (buffer ushr offset) + buffer = buffer and ((base - 1) shl offset).inv() + bufferBits -= bits + + result[r++] = alphabet[index] + } + } + if (bufferBits > 0) { + buffer = buffer shl (bits - bufferBits) + val index = buffer + result[r++] = alphabet[index] + } + + if (padding != null) { + while (r < result.size) { + result[r++] = padding + } + } + + return result + } + + override fun decode(bytes: ByteArray): ByteArray { + val bits = log2(base).toInt() + val index = padding?.let { bytes.indexOf(it) } ?: -1 + val decodedSize = when (index) { + -1 -> bytes.size * bits / 8 + else -> index * bits / 8 + } + val result = ByteArray(decodedSize) + + var r = 0 + var buffer = 0 + var bufferBits = 0 + + for (i in bytes.indices) { + val byte = bytes[i] + if (byte == padding) break + + val index = alphabet.indexOf(byte) + if (index == -1) throw Exception() //TODO + + buffer = (buffer shl bits) + index.toByte() + bufferBits += bits + + if (bufferBits >= 8) { + val offset = bufferBits - 8 + val character = (buffer ushr offset).toByte() + buffer = buffer and (0xFF shl offset).inv() + bufferBits -= 8 + + result[r++] = character + } + } + + return result + } +} diff --git a/src/commonMain/kotlin/com/infendro/encoding/util/BigInt.kt b/src/commonMain/kotlin/com/infendro/encoding/util/BigInt.kt new file mode 100644 index 0000000..d78fd09 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/encoding/util/BigInt.kt @@ -0,0 +1,89 @@ +package com.infendro.encoding.util + +internal class BigInt private constructor( + val words: IntArray, +) { + init { + require(words.isNotEmpty()) + } + + fun isZero(): Boolean = words.all { it == 0 } + + fun timesAdd(multiplier: Int, addend: Int): BigInt { + val product = IntArray(words.size) + var carry = addend + + for (i in words.indices) { + val current = (words[i].toLong() and 0xFFFFFFFFL) * multiplier + carry + product[i] = current.toInt() + carry = (current ushr 32).toInt() + } + + when { + carry > 0 -> { + val expanded = product.copyOf(product.size + 1) + expanded[product.size] = carry + return BigInt(expanded) + } + else -> return BigInt(product) + } + } + + fun divRem(divisor: Int): Pair { + val quotient = IntArray(words.size) + var remainder = 0 + + for (i in words.indices.reversed()) { + val current = (remainder.toLong() shl 32) or (words[i].toLong() and 0xFFFFFFFFL) + quotient[i] = (current / divisor.toLong()).toInt() + remainder = (current % divisor.toLong()).toInt() + } + + when { + quotient.size > 1 && quotient.last() == 0 -> { + val contracted = quotient.copyOfRange(0, quotient.size - 1) + return Pair(BigInt(contracted), remainder) + } + else -> return Pair(BigInt(quotient), remainder) + } + } + + fun toByteArray(): ByteArray { + val msw = words.last() + val mswBytes = when { + (msw ushr 24) != 0 -> 4 + (msw ushr 16) != 0 -> 3 + (msw ushr 8) != 0 -> 2 + else -> 1 + } + val size = ((words.size - 1) * 4) + mswBytes + val result = ByteArray(size) + + for (i in 0..