Skip to content

Commit

Permalink
feat(base45): add base45 encode and decode (#54)
Browse files Browse the repository at this point in the history
* 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
wafer-li and dependabot[bot] authored Jul 6, 2023
1 parent 381561c commit bad7847
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 3 deletions.
14 changes: 13 additions & 1 deletion README-zh.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Diglol Encoding

Diglol Encoding 为 Kotlin Multiplatform 提供了 Hex/Base16、Base32、Base64 编码。
Diglol Encoding 为 Kotlin Multiplatform 提供了 Hex/Base16、Base32、Base45、Base64 编码。

当前支持的编码:

- Hex (Base16)
- Base32 [Std, Hex]
- Base45
- Base64 [Std, Url]

### 发布
Expand Down Expand Up @@ -44,6 +45,16 @@ println(base32HexString)
assert(data.contentEquals(base32HexString.decodeBase32HexToBytes()))
```

##### [Base45][rfc9285]

```kotlin
val data = "base-45".encodeToByteArray()
val base45String = data.encodeBase45ToString()
println(base45String)
// 输出:UJCLQE7W581
assert(data.contentEquals(base45String.decodeBase45ToBytes()))
```

##### [Base64][rfc4648]

```kotlin
Expand Down Expand Up @@ -75,3 +86,4 @@ assert(data.contentEquals(base64UrlString.decodeBase64UrlToBytes()))
limitations under the License.

[rfc4648]: https://datatracker.ietf.org/doc/html/rfc4648
[rfc9285]: https://datatracker.ietf.org/doc/html/rfc9285
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Diglol Encoding

Diglol Encoding provides Hex/Base16, Base32, Base64 encodings for Kotlin Multiplatform.
Diglol Encoding provides Hex/Base16, Base32, Base45, Base64 encodings for Kotlin Multiplatform.
Translations: [中文](README-zh.md)

Currently supported encodings:

- Hex(Base16)
- Base32 (Std, Hex)
- Base45
- Base64 (Std, Url)

### Releases
Expand Down Expand Up @@ -45,6 +46,16 @@ println(base32HexString)
assert(data.contentEquals(base32HexString.decodeBase32HexToBytes()))
```

##### [Base45][rfc9285]

```kotlin
val data = "base-45".encodeToByteArray()
val base45String = data.encodeBase45ToString()
println(base45String)
// 输出:UJCLQE7W581
assert(data.contentEquals(base45String.decodeBase45ToBytes()))
```

##### [Base64][rfc4648]

```kotlin
Expand Down Expand Up @@ -76,3 +87,4 @@ assert(data.contentEquals(base64UrlString.decodeBase64UrlToBytes()))
limitations under the License.

[rfc4648]: https://datatracker.ietf.org/doc/html/rfc4648
[rfc9285]: https://datatracker.ietf.org/doc/html/rfc9285
44 changes: 44 additions & 0 deletions benchmark/src/commonMain/kotlin/diglol/encoding/Base45Benchmark.kt
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()
}
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ allprojects {
publishToMavenCentral(SonatypeHost.S01)
signAllPublications()
pom {
description.set("Diglol Encoding provides Hex/Base16, Base32, Base64 encodings for Kotlin Multiplatform.")
description.set("Diglol Encoding provides Hex/Base16, Base32, Base45, Base64 encodings for Kotlin Multiplatform.")
name.set(project.name)
url.set("https://github.com/diglol/encoding/")
licenses {
Expand Down
12 changes: 12 additions & 0 deletions encoding/src/androidMain/kotlin/diglol/encoding/base45.kt
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()
7 changes: 7 additions & 0 deletions encoding/src/commonMain/kotlin/diglol/encoding/base45.kt
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?
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 encoding/src/commonTest/kotlin/diglol/encoding/Base45Test.kt
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())
}
}
}
}
15 changes: 15 additions & 0 deletions encoding/src/jsMain/kotlin/diglol/encoding/base45.kt
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()
12 changes: 12 additions & 0 deletions encoding/src/jvmMain/kotlin/diglol/encoding/base45.kt
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()
10 changes: 10 additions & 0 deletions encoding/src/nativeMain/kotlin/diglol/encoding/base45.kt
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()

0 comments on commit bad7847

Please sign in to comment.