-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(base45): add base45 encode and decode (#54)
* Bump com.vanniktech:gradle-maven-publish-plugin from 0.25.2 to 0.25.3 (#55) Bumps [com.vanniktech:gradle-maven-publish-plugin](https://github.com/vanniktech/gradle-maven-publish-plugin) from 0.25.2 to 0.25.3. - [Release notes](https://github.com/vanniktech/gradle-maven-publish-plugin/releases) - [Changelog](https://github.com/vanniktech/gradle-maven-publish-plugin/blob/main/CHANGELOG.md) - [Commits](vanniktech/gradle-maven-publish-plugin@0.25.2...0.25.3) --- updated-dependencies: - dependency-name: com.vanniktech:gradle-maven-publish-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: add base45 encode * refactor: base45 use expect-actual * feat: add base45 decode * test: add test for invalid decode * refactor: base 45 decode use expect-actual * test: remove unnecessary ? * docs: add base45 description * test: replace improper test case * refactor: change decode to string as decode to bytes * feat: add benchmark * build: pom add description of Base45 * refactor: androidMain add file name base45 Refs: #54 (comment) * refactor: jvmMain add file name base45 Refs: #54 (comment) * style: remove unnecessary empty line Refs: #54 (comment) * style: remove unnecessary empty line in common main base45.kt Refs: #54 (comment) * refactor: inline isEncodedSizeNotValid Refs: #54 (comment) * refactor: inline sizeOfEncodedBase45 Refs: #54 (comment) * build: re-arrange base45 and base64 Refs: #54 (comment) * refactor: manually inline isEncodeSizeNotValid Refs:#54 (comment) * refactor: manually inline sizeOfEncodedBase45 Refs: #54 (comment) * fix: typo Refs: #54 (comment) * refactor: remove unnecessary in and IntRange Refs: #54 (comment) * docs: add base45 in README.md Refs: #54 (comment) * docs: add link for rfc9285 Refs: #54 (comment) * style: remove unnecessary empty line Refs: #54 (comment) * refactor: remove map param in commonEncodeBase45 Refs: #54 (comment) * fix: compile error due to leaving BASE45 map somewhere --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
- Loading branch information
1 parent
381561c
commit bad7847
Showing
11 changed files
with
319 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
benchmark/src/commonMain/kotlin/diglol/encoding/Base45Benchmark.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package diglol.encoding | ||
|
||
import kotlin.random.Random | ||
import kotlinx.benchmark.Benchmark | ||
import kotlinx.benchmark.BenchmarkMode | ||
import kotlinx.benchmark.BenchmarkTimeUnit | ||
import kotlinx.benchmark.Measurement | ||
import kotlinx.benchmark.Mode | ||
import kotlinx.benchmark.OutputTimeUnit | ||
import kotlinx.benchmark.Param | ||
import kotlinx.benchmark.Scope | ||
import kotlinx.benchmark.Setup | ||
import kotlinx.benchmark.State | ||
|
||
@State(Scope.Benchmark) | ||
@Measurement(iterations = 5, time = 1) | ||
@BenchmarkMode(Mode.AverageTime) | ||
@OutputTimeUnit(BenchmarkTimeUnit.MICROSECONDS) | ||
class Base45Benchmark { | ||
@Param("1024", "10240", "102400") | ||
var dataSize = 0 | ||
private lateinit var data: ByteArray | ||
private lateinit var base45: ByteArray | ||
private lateinit var base45String: String | ||
|
||
@Setup | ||
fun setup() { | ||
data = Random.nextBytes(dataSize) | ||
base45 = data.encodeBase45() | ||
base45String = data.encodeBase45ToString() | ||
} | ||
|
||
@Benchmark | ||
fun encodeBase45() = data.encodeBase45() | ||
|
||
@Benchmark | ||
fun decodeBase45() = base45.decodeBase45() | ||
|
||
@Benchmark | ||
fun encodeBase45ToString() = data.encodeBase45ToString() | ||
|
||
@Benchmark | ||
fun decodeBase45ToBytes() = base45String.decodeBase45ToBytes() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
@file:JvmName("Base45") | ||
|
||
package diglol.encoding | ||
|
||
import diglol.encoding.internal.commonDecodeBase45 | ||
import diglol.encoding.internal.commonEncodeBase45 | ||
|
||
actual fun ByteArray.encodeBase45(): ByteArray = commonEncodeBase45() | ||
actual fun ByteArray.decodeBase45(): ByteArray? = commonDecodeBase45() | ||
|
||
actual fun ByteArray.encodeBase45ToString(): String = commonEncodeBase45().decodeToString() | ||
actual fun String.decodeBase45ToBytes(): ByteArray? = encodeToByteArray().decodeBase45() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package diglol.encoding | ||
|
||
expect fun ByteArray.encodeBase45(): ByteArray | ||
expect fun ByteArray.decodeBase45(): ByteArray? | ||
|
||
expect fun ByteArray.encodeBase45ToString(): String | ||
expect fun String.decodeBase45ToBytes(): ByteArray? |
121 changes: 121 additions & 0 deletions
121
encoding/src/commonMain/kotlin/diglol/encoding/internal/commonBase45.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package diglol.encoding.internal | ||
|
||
import kotlin.native.concurrent.SharedImmutable | ||
|
||
/** | ||
* Implementation of Base45 encoding/decoding. | ||
* | ||
* Basically, Base45 is just encode 2 bytes based on 256 into 3 bytes based on 45. | ||
* | ||
* It will result in a encoded string which will generate a compat QR code when using alphanumeric mode. | ||
* | ||
* @see [RFC 9285](https://datatracker.ietf.org/doc/rfc9285/) | ||
*/ | ||
|
||
@SharedImmutable | ||
private val BASE45 = byteArrayOf( | ||
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, // 0-9 | ||
65, 66, 67, 68, 69, 70, 71, 72, 73, 74, // A-J | ||
75, 76, 77, 78, 79, 80, 81, 82, 83, 84, // K-T | ||
85, 86, 87, 88, 89, 90, // U-Z | ||
// SP, $, %, *, +, -, ., /, : | ||
32, 36, 37, 42, 43, 45, 46, 47, 58 | ||
) | ||
|
||
internal fun ByteArray.commonEncodeBase45(): ByteArray { | ||
val outSize = when { | ||
isEmpty() -> 0 | ||
size % 2 == 0 -> size * 3 / 2 | ||
else -> (size - 1) * 3 / 2 + 2 | ||
} | ||
val outBytes = ByteArray(outSize) | ||
val limit = if (size % 2 == 0) size else size - 1 | ||
|
||
var i = 0 | ||
var j = 0 | ||
|
||
while (i < limit) { | ||
val a = this[i].toInt() and 0xff | ||
val b = this[i + 1].toInt() and 0xff | ||
|
||
val n = (a shl 8) + b | ||
|
||
val c = n % 45 | ||
val d = (n / 45) % 45 | ||
val e = ((n / 45) / 45) % 45 | ||
|
||
outBytes[j] = BASE45[c] | ||
outBytes[j + 1] = BASE45[d] | ||
outBytes[j + 2] = BASE45[e] | ||
|
||
i += 2 | ||
j += 3 | ||
} | ||
|
||
if (limit < size) { | ||
val a = this[limit].toInt() and 0xff | ||
val c = a % 45 | ||
val d = (a / 45) % 45 | ||
outBytes[j] = BASE45[c] | ||
outBytes[j + 1] = BASE45[d] | ||
} | ||
|
||
return outBytes | ||
} | ||
|
||
internal fun ByteArray.commonDecodeBase45(): ByteArray? { | ||
if (size % 3 == 1) { | ||
return null | ||
} | ||
|
||
val limit = if (size % 3 == 0) size else size - 2 | ||
val outSize = if (size % 3 == 0) size / 3 * 2 else (size - 2) / 3 * 2 + 1 | ||
|
||
val outBytes = ByteArray(outSize) | ||
|
||
var i = 0 | ||
var j = 0 | ||
|
||
while (i < limit) { | ||
|
||
val c = this[i].toBase45Code() ?: return null | ||
val d = this[i + 1].toBase45Code() ?: return null | ||
val e = this[i + 2].toBase45Code() ?: return null | ||
|
||
val n = c + d * 45 + e * 45 * 45 | ||
|
||
val a = n ushr 8 | ||
val b = n and 0xff | ||
|
||
outBytes[j] = a.toByte() | ||
outBytes[j + 1] = b.toByte() | ||
|
||
i += 3 | ||
j += 2 | ||
} | ||
|
||
if (limit < size) { | ||
val c = this[limit].toBase45Code() ?: return null | ||
val d = this[limit + 1].toBase45Code() ?: return null | ||
val a = c + d * 45 | ||
outBytes[j] = a.toByte() | ||
} | ||
|
||
return outBytes | ||
} | ||
|
||
/** | ||
* Converts a char's byte code to a Base45 lookup table index. | ||
*/ | ||
private fun Byte.toBase45Code(): Int? { | ||
return when (this.toInt() and 0xff) { | ||
32 -> 36 | ||
58 -> 44 | ||
36, 37 -> this + 1 | ||
42, 43 -> this - 3 | ||
45, 46, 47 -> this - 4 | ||
in '0'.code..'9'.code -> this - '0'.code | ||
in 'A'.code..'Z'.code -> this - 'A'.code + 10 | ||
else -> null | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
encoding/src/commonTest/kotlin/diglol/encoding/Base45Test.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package diglol.encoding | ||
|
||
import kotlin.test.Test | ||
import kotlin.test.assertContentEquals | ||
import kotlin.test.assertEquals | ||
|
||
class Base45Test { | ||
|
||
private val sample = mapOf( | ||
"AB" to "BB8", | ||
"Hello!!" to "%69 VD92EX0", | ||
"base-45" to "UJCLQE7W581", | ||
"ietf!" to "QED8WEX0", | ||
"""the quick brown fox jumps over the lazy dog THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG 1234567890-=[];',./\!@#$%^&*()_+{}|:"<>?""" to | ||
"""AWEDZCKFEOEDJOD2KC54EM-DX.CH8FSKDQ${'$'}D.OE44E5${'$'}CS44+8DK44OEC3EFGVCU1DLTABX8VCAZB9HM9DH8G1AK*9:*8F6B*H9${'$'}Y9+MAF1AGY8-34669X34ZB81CBRS8S:8*96DL6WW66:6FA7GW5YOBNL7FQ5J:5794-J4QW45${'$'}4L35I1CNRFWVFYE45*7""", | ||
) | ||
|
||
private val sampleBytes = sample.map { (origin, encoded) -> | ||
origin.encodeToByteArray() to encoded.encodeToByteArray() | ||
}.toMap() | ||
|
||
private val invalidSample = mapOf( | ||
"|~{}" to null, | ||
0x0.toChar().toString() to null, | ||
) | ||
|
||
@Test | ||
fun testEncodeBase45() { | ||
sampleBytes.forEach { (origin, expectedEncoded) -> | ||
ignoreNpe { | ||
assertContentEquals(expectedEncoded, origin.encodeBase45()) | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
fun testEncodedBase45String() { | ||
sample.forEach { (origin, expected) -> | ||
ignoreNpe { | ||
assertEquals(expected, origin.encodeToByteArray().encodeBase45ToString()) | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
fun testDecodeBase45() { | ||
sampleBytes.forEach { (expectedOrigin, encoded) -> | ||
ignoreNpe { | ||
assertContentEquals(expectedOrigin, encoded.decodeBase45()) | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
fun testDecodeBase45ToBytes() { | ||
sample.forEach { (expectedOrigin, encoded) -> | ||
ignoreNpe { | ||
assertContentEquals(expectedOrigin.encodeToByteArray(), encoded.decodeBase45ToBytes()) | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
fun testInvalidDecodeBase45() { | ||
invalidSample.forEach { (encoded, expectedOrigin) -> | ||
ignoreNpe { | ||
assertContentEquals(expectedOrigin, encoded.decodeBase45ToBytes()) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
@file:OptIn(InternalApi::class) | ||
|
||
package diglol.encoding | ||
|
||
import diglol.encoding.internal.commonDecodeBase45 | ||
import diglol.encoding.internal.commonEncodeBase45 | ||
import diglol.encoding.internal.jsDecodeToString | ||
import diglol.encoding.internal.jsEncodeToByteArray | ||
|
||
actual fun ByteArray.encodeBase45(): ByteArray = commonEncodeBase45() | ||
|
||
actual fun ByteArray.encodeBase45ToString(): String = commonEncodeBase45().jsDecodeToString() | ||
|
||
actual fun ByteArray.decodeBase45(): ByteArray? = commonDecodeBase45() | ||
actual fun String.decodeBase45ToBytes(): ByteArray? = jsEncodeToByteArray().decodeBase45() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
@file:JvmName("Base45") | ||
|
||
package diglol.encoding | ||
|
||
import diglol.encoding.internal.commonDecodeBase45 | ||
import diglol.encoding.internal.commonEncodeBase45 | ||
|
||
actual fun ByteArray.encodeBase45(): ByteArray = commonEncodeBase45() | ||
actual fun ByteArray.decodeBase45(): ByteArray? = commonDecodeBase45() | ||
|
||
actual fun ByteArray.encodeBase45ToString(): String = commonEncodeBase45().decodeToString() | ||
actual fun String.decodeBase45ToBytes(): ByteArray? = encodeToByteArray().decodeBase45() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package diglol.encoding | ||
|
||
import diglol.encoding.internal.commonDecodeBase45 | ||
import diglol.encoding.internal.commonEncodeBase45 | ||
|
||
actual fun ByteArray.encodeBase45(): ByteArray = commonEncodeBase45() | ||
actual fun ByteArray.decodeBase45(): ByteArray? = commonDecodeBase45() | ||
|
||
actual fun ByteArray.encodeBase45ToString(): String = commonEncodeBase45().decodeToString() | ||
actual fun String.decodeBase45ToBytes(): ByteArray? = encodeToByteArray().decodeBase45() |