From 6283c2f44df12917de47fcbb856e7921c9e46bd1 Mon Sep 17 00:00:00 2001 From: Infendro Date: Tue, 20 Jan 2026 16:59:05 +0100 Subject: [PATCH] refactor encodings --- .../kotlin/com/infendro/encoding/Base16.kt | 9 ++- .../kotlin/com/infendro/encoding/Base32.kt | 2 +- .../kotlin/com/infendro/encoding/Base64.kt | 9 ++- .../kotlin/com/infendro/encoding/Encoding.kt | 4 - .../com/infendro/encoding/NumericEncoding.kt | 6 +- .../infendro/encoding/PaddedStreamEncoding.kt | 78 ------------------- .../com/infendro/encoding/StreamEncoding.kt | 78 +++++++++++-------- .../com/infendro/encoding/util/BigInt.kt | 36 +++------ .../kotlin/com/infendro/encoding/util/Int.kt | 7 ++ .../kotlin/com/infendro/encoding/util/Math.kt | 6 +- .../kotlin/com/infendro/encoding/Base56.kt | 2 +- 11 files changed, 83 insertions(+), 154 deletions(-) delete mode 100644 src/commonMain/kotlin/com/infendro/encoding/PaddedStreamEncoding.kt create mode 100644 src/commonMain/kotlin/com/infendro/encoding/util/Int.kt diff --git a/src/commonMain/kotlin/com/infendro/encoding/Base16.kt b/src/commonMain/kotlin/com/infendro/encoding/Base16.kt index eff1268..6530b7c 100644 --- a/src/commonMain/kotlin/com/infendro/encoding/Base16.kt +++ b/src/commonMain/kotlin/com/infendro/encoding/Base16.kt @@ -1,5 +1,10 @@ package com.infendro.encoding -private val alphabet = "0123456789ABCDEF".encodeToByteArray() +private val uppercase = "0123456789ABCDEF".encodeToByteArray() +private val lowercase = "0123456789abcdef".encodeToByteArray() -object Base16 : Encoding by StreamEncoding(alphabet) +object Base16 : Encoding by StreamEncoding(uppercase) { + val UPPERCASE = this + + object LOWERCASE : Encoding by StreamEncoding(lowercase) +} diff --git a/src/commonMain/kotlin/com/infendro/encoding/Base32.kt b/src/commonMain/kotlin/com/infendro/encoding/Base32.kt index d30a863..b89a8cb 100644 --- a/src/commonMain/kotlin/com/infendro/encoding/Base32.kt +++ b/src/commonMain/kotlin/com/infendro/encoding/Base32.kt @@ -3,4 +3,4 @@ package com.infendro.encoding private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".encodeToByteArray() private const val padding = '='.code.toByte() -object Base32 : Encoding by PaddedStreamEncoding(alphabet, padding) +object Base32 : Encoding by StreamEncoding(alphabet, padding) diff --git a/src/commonMain/kotlin/com/infendro/encoding/Base64.kt b/src/commonMain/kotlin/com/infendro/encoding/Base64.kt index 2dfe8b1..2ef7ab0 100644 --- a/src/commonMain/kotlin/com/infendro/encoding/Base64.kt +++ b/src/commonMain/kotlin/com/infendro/encoding/Base64.kt @@ -1,6 +1,11 @@ package com.infendro.encoding -private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".encodeToByteArray() +private val standard = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".encodeToByteArray() +private val url = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".encodeToByteArray() private const val padding = '='.code.toByte() -object Base64 : Encoding by PaddedStreamEncoding(alphabet, padding) +object Base64 : Encoding by StreamEncoding(standard, padding) { + val STANDARD = this + + object URL : Encoding by StreamEncoding(url) +} diff --git a/src/commonMain/kotlin/com/infendro/encoding/Encoding.kt b/src/commonMain/kotlin/com/infendro/encoding/Encoding.kt index 6949e9b..d302e59 100644 --- a/src/commonMain/kotlin/com/infendro/encoding/Encoding.kt +++ b/src/commonMain/kotlin/com/infendro/encoding/Encoding.kt @@ -1,10 +1,6 @@ package com.infendro.encoding interface Encoding { - val alphabet: ByteArray - val base: Int - get() = alphabet.size - fun encode(bytes: ByteArray): ByteArray fun decode(bytes: ByteArray): ByteArray } diff --git a/src/commonMain/kotlin/com/infendro/encoding/NumericEncoding.kt b/src/commonMain/kotlin/com/infendro/encoding/NumericEncoding.kt index b360248..36e9555 100644 --- a/src/commonMain/kotlin/com/infendro/encoding/NumericEncoding.kt +++ b/src/commonMain/kotlin/com/infendro/encoding/NumericEncoding.kt @@ -4,8 +4,10 @@ import com.infendro.encoding.exception.IllegalByteException import com.infendro.encoding.util.BigInt class NumericEncoding internal constructor( - override val alphabet: ByteArray, + private val alphabet: ByteArray, ) : Encoding { + private val base: Int = alphabet.size + override fun encode(bytes: ByteArray): ByteArray { if (bytes.isEmpty()) return byteArrayOf() @@ -27,10 +29,8 @@ class NumericEncoding internal constructor( var value = BigInt.zero for (i in 0..= 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) { - val index = buffer shl (bits - bufferBits) - result[r++] = alphabet[index] - } - while (r < result.size) { - result[r++] = padding - } - - return result - } - - override fun decode(bytes: ByteArray): ByteArray { - val bits = log2(base).toInt() - - val end = bytes.indexOf(padding).takeIf { it != -1 } ?: bytes.size - val size = end * bits / 8 - val result = ByteArray(size) - - var r = 0 - var buffer = 0 - var bufferBits = 0 - for (i in 0..= 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/StreamEncoding.kt b/src/commonMain/kotlin/com/infendro/encoding/StreamEncoding.kt index daa5cb8..e4ad829 100644 --- a/src/commonMain/kotlin/com/infendro/encoding/StreamEncoding.kt +++ b/src/commonMain/kotlin/com/infendro/encoding/StreamEncoding.kt @@ -1,70 +1,80 @@ package com.infendro.encoding import com.infendro.encoding.exception.IllegalByteException +import com.infendro.encoding.util.align import com.infendro.encoding.util.divCeil +import com.infendro.encoding.util.lcm import com.infendro.encoding.util.log2 class StreamEncoding internal constructor( - override val alphabet: ByteArray, + private val alphabet: ByteArray, + private val padding: Byte? = null, ) : Encoding { + private val base: Int = alphabet.size + private val bits: Int = log2(base).toInt() + private val block: Int = lcm(bits, 8) / bits + private val mask: Int = (0x01 shl bits) - 1 + override fun encode(bytes: ByteArray): ByteArray { - val bits = log2(base).toInt() + val size = when (padding) { + null -> (bytes.size * 8).divCeil(bits) + else -> (bytes.size * 8).divCeil(bits).align(block) + } + val encoded = ByteArray(size) + var i = 0 - val size = (bytes.size * 8).divCeil(bits) - val result = ByteArray(size) - - var r = 0 var buffer = 0 var bufferBits = 0 - for (i in bytes.indices) { - buffer = (buffer shl 8) + bytes[i] + for (byte in bytes) { + buffer = (buffer shl 8) or (byte.toInt() and 0xFF) bufferBits += 8 while (bufferBits >= bits) { - val offset = bufferBits - bits - val index = (buffer ushr offset) - buffer = buffer and ((base - 1) shl offset).inv() + val index = (buffer shr (bufferBits - bits)) and mask bufferBits -= bits - - result[r++] = alphabet[index] + encoded[i++] = alphabet[index] } } if (bufferBits > 0) { - val index = buffer shl (bits - bufferBits) - result[r] = alphabet[index] + val index = (buffer shl (bits - bufferBits)) and mask + encoded[i++] = alphabet[index] } - return result + while (i < encoded.size) { + encoded[i++] = padding!! + } + + return encoded } override fun decode(bytes: ByteArray): ByteArray { - val bits = log2(base).toInt() + val end = when (padding) { + null -> bytes.size + else -> when (val index = bytes.indexOf(padding)) { + -1 -> bytes.size + else -> index + } + } + val size = end * bits / 8 + val decoded = ByteArray(size) + var i = 0 - val size = bytes.size * bits / 8 - val result = ByteArray(size) - - var r = 0 var buffer = 0 var bufferBits = 0 - for (i in bytes.indices) { - val byte = bytes[i] - - val value = alphabet.indexOf(byte) - if (value == -1) throw IllegalByteException(byte) - - buffer = (buffer shl bits) + value.toByte() + for (b in 0..= 8) { - val offset = bufferBits - 8 - val character = (buffer ushr offset).toByte() - buffer = buffer and (0xFF shl offset).inv() + val byte = (buffer shr (bufferBits - 8)) and 0xFF bufferBits -= 8 - - result[r++] = character + decoded[i++] = byte.toByte() } } - return result + return decoded } } diff --git a/src/commonMain/kotlin/com/infendro/encoding/util/BigInt.kt b/src/commonMain/kotlin/com/infendro/encoding/util/BigInt.kt index d78fd09..14c8d39 100644 --- a/src/commonMain/kotlin/com/infendro/encoding/util/BigInt.kt +++ b/src/commonMain/kotlin/com/infendro/encoding/util/BigInt.kt @@ -49,40 +49,24 @@ internal class BigInt private constructor( } 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 bytes = ByteArray((words.size * 4) - words.last().countLeadingZeroBytes()) + for (i in bytes.indices) { + val (wi, bi) = (bytes.lastIndex - i).divRem(4) + bytes[i] = (words[wi] ushr (bi * 8)).toByte() } - val size = ((words.size - 1) * 4) + mswBytes - val result = ByteArray(size) - - for (i in 0.. = + Pair(this / divisor, this % divisor) + +fun Int.countLeadingZeroBytes(): Int = + countLeadingZeroBits() / 8 diff --git a/src/commonMain/kotlin/com/infendro/encoding/util/Math.kt b/src/commonMain/kotlin/com/infendro/encoding/util/Math.kt index a590e8b..a5ea675 100644 --- a/src/commonMain/kotlin/com/infendro/encoding/util/Math.kt +++ b/src/commonMain/kotlin/com/infendro/encoding/util/Math.kt @@ -2,15 +2,15 @@ package com.infendro.encoding.util import kotlin.math.log2 -internal fun log2(value: Int): Double = - log2(value.toDouble()) - internal fun Int.divCeil(n: Int): Int = (this + n - 1) / n internal fun Int.align(n: Int): Int = divCeil(n) * n +internal fun log2(value: Int): Double = + log2(value.toDouble()) + internal fun gcd(a: Int, b: Int): Int { var x = a var y = b diff --git a/src/commonTest/kotlin/com/infendro/encoding/Base56.kt b/src/commonTest/kotlin/com/infendro/encoding/Base56.kt index 687ce11..9147642 100644 --- a/src/commonTest/kotlin/com/infendro/encoding/Base56.kt +++ b/src/commonTest/kotlin/com/infendro/encoding/Base56.kt @@ -24,7 +24,7 @@ class `Base56 Test` { @Test fun encode() { - val actual = encoding.encode(original).also { println(it.decodeToString()) } + val actual = encoding.encode(original) assertContentEquals(encoded, actual) }