Skip to content

Commit

Permalink
KTXIO-based number encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
JesusMcCloud committed Oct 17, 2024
1 parent f4454c9 commit a6bc66f
Show file tree
Hide file tree
Showing 8 changed files with 558 additions and 32 deletions.
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
core = "1.5.0"
kotlinxIoCore = "0.5.4"
multibase = "1.2.0"
kotestExtensionsAndroid = "0.1.3"
kotestRunnerAndroid = "1.1.1"
Expand All @@ -9,12 +10,14 @@ jose = "9.31"
kotlinpoet = "1.16.0"
runner = "1.5.2"
kotest-plugin = "20240918.002009-71"
kmmresult = "1.8.0"

[libraries]
bignum = { group = "com.ionspin.kotlin", name = "bignum", version.ref = "bignum" }
core = { module = "androidx.test:core", version.ref = "core" }
kotest-extensions-android = { module = "br.com.colman:kotest-extensions-android", version.ref = "kotestExtensionsAndroid" }
kotest-runner-android = { module = "br.com.colman:kotest-runner-android", version.ref = "kotestRunnerAndroid" }
kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIoCore" }
okio = { group = "com.squareup.okio", name = "okio", version.ref = "okio" }
jose = { group = "com.nimbusds", name = "nimbus-jose-jwt", version.ref = "jose" }
kotlinpoet = { group = "com.squareup", name = "kotlinpoet", version.ref = "kotlinpoet" }
Expand Down
1 change: 1 addition & 0 deletions indispensable/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ kotlin {
)

dependencies {
api(libs.kotlinx.io.core)
api(kmmresult())
api(serialization("json"))
api(datetime())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import at.asitplus.signum.indispensable.io.ByteArrayBase64Serializer
import io.matthewnelson.encoding.base16.Base16
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.io.Buffer
import kotlinx.io.Sink
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
Expand Down Expand Up @@ -748,4 +750,20 @@ internal fun Int.encodeLength(): ByteArray {
byteArrayOf((lengthLength or 0x80).toByte(), *length)
}
}
}

@Throws(IllegalArgumentException::class)
internal fun Sink.encodeLength(len: Long): Int {
require(len >= 0)
return when {
(len < 0x80) -> writeByte(len.toByte()).run { 1 } /* short form */
else -> { /* long form */
val length = Buffer()
val lengthLength = length.writeUnsignedTwosComplementLong(len)
check(lengthLength < 0x80)
writeByte((lengthLength or 0x80).toByte())
length.transferTo(this)
1 + lengthLength
}
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import io.matthewnelson.encoding.base64.Base64
import io.matthewnelson.encoding.base64.Base64ConfigBuilder
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.io.Buffer
import kotlinx.io.bytestring.ByteString
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
Expand Down Expand Up @@ -122,4 +124,13 @@ fun ByteArray.ensureSize(size: Int): ByteArray = (this.size - size).let { toDrop
}

@Suppress("NOTHING_TO_INLINE")
inline fun ByteArray.ensureSize(size: UInt) = ensureSize(size.toInt())
inline fun ByteArray.ensureSize(size: UInt) = ensureSize(size.toInt())

/**
* gets the first byte
*/
@Suppress("NOTHING_TO_INLINE")
inline fun ByteString.first() = get(0)

@Suppress("NOTHING_TO_INLINE")
inline fun ByteArray.asBuffer()=Buffer().also{it.write(this)}
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package at.asitplus.signum.indispensable

import at.asitplus.signum.indispensable.asn1.*
import at.asitplus.signum.indispensable.asn1.encoding.*
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.BitString
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Bool
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.ExplicitlyTagged
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Null
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.OctetString
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.OctetStringEncapsulating
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.PrintableString
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.ExplicitlyTagged
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.UtcTime
import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Utf8String
import at.asitplus.signum.indispensable.asn1.encoding.*
import at.asitplus.signum.indispensable.io.BitSet
import at.asitplus.signum.indispensable.io.asBuffer
import com.ionspin.kotlin.bignum.integer.BigInteger
import com.ionspin.kotlin.bignum.integer.base63.toJavaBigInteger
import com.ionspin.kotlin.bignum.integer.toBigInteger
import com.ionspin.kotlin.bignum.integer.util.fromTwosComplementByteArray
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FreeSpec
import io.kotest.datatest.withData
import io.kotest.matchers.nulls.shouldNotBeNull
Expand All @@ -25,8 +27,11 @@ import io.kotest.property.Arb
import io.kotest.property.arbitrary.*
import io.kotest.property.checkAll
import kotlinx.datetime.Clock
import kotlinx.io.Buffer
import kotlinx.io.snapshot
import org.bouncycastle.asn1.ASN1Integer
import java.util.*
import kotlin.math.pow

class Asn1EncodingTest : FreeSpec({

Expand Down Expand Up @@ -89,15 +94,32 @@ class Asn1EncodingTest : FreeSpec({

"Asn1 Number encoding" - {

withData(15253481L, -1446230472L, 0L, 1L, -1L, -2L, -9994587L, 340281555L) {
val bytes = (it).toTwosComplementByteArray()
"manual" - {
withData(
257L,
2f.pow(24).toLong() - 1,
65555,
2f.pow(24).toLong(),
15253481L,
-1446230472L,
0L,
1L,
-1L,
-2L,
-9994587L,
340281555L
) {
val bytes = (it).toTwosComplementByteArray()

val fromBC = ASN1Integer(it).encoded
val long = Long.decodeFromAsn1ContentBytes(bytes)

val fromBC = ASN1Integer(it).encoded
val long = Long.decodeFromAsn1ContentBytes(bytes)
val encoded = Asn1Primitive(Asn1Element.Tag.INT, bytes).derEncoded
encoded shouldBe fromBC
long shouldBe it

val encoded = Asn1Primitive(Asn1Element.Tag.INT, bytes).derEncoded
encoded shouldBe fromBC
long shouldBe it
bytes.asBuffer().readTwosComplementLong() shouldBe it
}
}


Expand All @@ -121,6 +143,11 @@ class Asn1EncodingTest : FreeSpec({
decoded shouldBe it

Asn1.Int(it).derEncoded shouldBe ASN1Integer(it).encoded

it.toTwosComplementByteArray().asBuffer().readTwosComplementLong() shouldBe it
Buffer().apply { writeTwosComplementLong(it) }.snapshot()
.toByteArray() shouldBe it.toTwosComplementByteArray()

}
}
}
Expand All @@ -143,6 +170,8 @@ class Asn1EncodingTest : FreeSpec({
decoded shouldBe it

Asn1.Int(it).derEncoded shouldBe ASN1Integer(it.toLong()).encoded
it.toTwosComplementByteArray().asBuffer().readTwosComplementInt() shouldBe it
it.toTwosComplementByteArray().asBuffer().readTwosComplementLong() shouldBe it
}
}
}
Expand All @@ -165,11 +194,29 @@ class Asn1EncodingTest : FreeSpec({
decoded shouldBe it

Asn1.Int(it).derEncoded shouldBe ASN1Integer(it.toBigInteger().toJavaBigInteger()).encoded
it.toTwosComplementByteArray().asBuffer().readTwosComplementUInt() shouldBe it
it.toTwosComplementByteArray().asBuffer().readTwosComplementULong() shouldBe it.toULong()
}
}
}

"unsigned longs" - {

"manual" - {
withData(
2f.pow(24).toULong() - 1u,
256uL,
65555uL,
2f.pow(24).toULong(),
255uL,
360uL,
4113774321109173852uL
) {
val bytes = (it).toTwosComplementByteArray()
bytes.asBuffer().readTwosComplementULong() shouldBe it
}
}

"failures: negative" - {
checkAll(iterations = 5000, Arb.long(Long.MIN_VALUE..<0)) {
shouldThrow<Asn1Exception> { Asn1.Int(it).decodeToULong() }
Expand All @@ -188,6 +235,7 @@ class Asn1EncodingTest : FreeSpec({
decoded shouldBe it

Asn1.Int(it).derEncoded shouldBe ASN1Integer(it.toBigInteger().toJavaBigInteger()).encoded
it.toTwosComplementByteArray().asBuffer().readTwosComplementULong() shouldBe it
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import io.kotest.datatest.withData
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.positiveInt
import io.kotest.property.arbitrary.uInt
import io.kotest.property.arbitrary.uLong
import io.kotest.property.checkAll
import kotlinx.io.Buffer
import kotlinx.io.snapshot
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DERTaggedObject

Expand All @@ -28,6 +31,12 @@ class TagEncodingTest : FreeSpec({
long shouldBe it
}

"length encoding" - {
checkAll(Arb.positiveInt()) {
Buffer().apply { encodeLength(it.toLong()) }.snapshot().toByteArray() shouldBe it.encodeLength()
}
}

"Manual" - {
withData(207692171uL, 128uL, 36uL, 16088548868045964978uL, 15871772363588580035uL) {
it.toAsn1VarInt().decodeAsn1VarULong().first shouldBe it
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,65 @@
package at.asitplus.signum.indispensable

import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarBigInt
import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarUInt
import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarULong
import at.asitplus.signum.indispensable.asn1.encoding.toAsn1VarInt
import at.asitplus.signum.indispensable.asn1.encoding.*
import at.asitplus.signum.indispensable.io.asBuffer
import com.ionspin.kotlin.bignum.integer.BigInteger
import com.ionspin.kotlin.bignum.integer.Sign
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.bigInt
import io.kotest.property.arbitrary.uInt
import io.kotest.property.arbitrary.uLong
import io.kotest.property.checkAll
import kotlinx.io.Buffer
import kotlinx.io.snapshot
import kotlinx.io.writeULong
import kotlin.random.Random

class UVarIntTest : FreeSpec({

"UInts with trailing bytes" - {
"manual" {
byteArrayOf(65, 0, 0, 0).decodeAsn1VarUInt().first shouldBe 65u
val src = byteArrayOf(65, 0, 0, 0)
src.decodeAsn1VarUInt().first shouldBe 65u
val buf = src.asBuffer()
buf.decodeAsn1VarUInt().first shouldBe 65u
repeat(3){buf.readByte() shouldBe 0.toByte()}
buf.exhausted().shouldBeTrue()
}
"automated -" {
checkAll(Arb.uInt()) { int ->
(int.toAsn1VarInt().asList() + Random.nextBytes(8).asList()).decodeAsn1VarUInt().first shouldBe int

val rnd = Random.nextBytes(8)
val src = int.toAsn1VarInt().asList() + rnd.asList()
src.decodeAsn1VarUInt().first shouldBe int
val buffer = src.toByteArray().asBuffer()
buffer.decodeAsn1VarUInt().first shouldBe int
rnd.forEach { it shouldBe buffer.readByte() }
buffer.exhausted().shouldBeTrue()
}
}
}

"ULongs with trailing bytes" - {
"manual" {
byteArrayOf(65, 0, 0, 0).decodeAsn1VarULong().first shouldBe 65uL
val src = byteArrayOf(65, 0, 0, 0)
src.decodeAsn1VarULong().first shouldBe 65uL
val buf = src.asBuffer()
buf.decodeAsn1VarULong().first shouldBe 65uL
repeat(3){buf.readByte() shouldBe 0.toByte()}
buf.exhausted().shouldBeTrue()
}
"automated -" {
checkAll(Arb.uLong()) { long ->
(long.toAsn1VarInt().asList() + Random.nextBytes(8).asList()).decodeAsn1VarULong().first shouldBe long
val rnd = Random.nextBytes(8)
val src = long.toAsn1VarInt().asList() + rnd.asList()
src.decodeAsn1VarULong().first shouldBe long

val buffer = src.toByteArray().asBuffer()
buffer.decodeAsn1VarULong().first shouldBe long
rnd.forEach { it shouldBe buffer.readByte() }
buffer.exhausted().shouldBeTrue()

}
}
Expand All @@ -49,17 +73,35 @@ class UVarIntTest : FreeSpec({
val bigIntVarInt = bigInteger.toAsn1VarInt()

bigIntVarInt shouldBe uLongVarInt
Buffer().apply { writeAsn1VarInt(bigInteger) }.snapshot().toByteArray() shouldBe bigIntVarInt
Buffer().apply { writeAsn1VarInt(long) }.snapshot().toByteArray() shouldBe uLongVarInt

val rnd = Random.nextBytes(8)
val src = uLongVarInt.asList() + rnd.asList()
src.decodeAsn1VarBigInt().first shouldBe bigInteger

(uLongVarInt.asList() + Random.nextBytes(8).asList()).decodeAsn1VarBigInt().first shouldBe bigInteger

val buffer = src.toByteArray().asBuffer()
buffer.decodeAsn1VarBigInt().first shouldBe bigInteger
rnd.forEach { it shouldBe buffer.readByte() }
buffer.exhausted().shouldBeTrue()

}
}

"larger" - {
checkAll(Arb.bigInt(1, 1024 * 32)) { javaBigInt ->
val bigInt = BigInteger.fromByteArray(javaBigInt.toByteArray(), Sign.POSITIVE)
(bigInt.toAsn1VarInt().asList() + Random.nextBytes(33)
.asList()).decodeAsn1VarBigInt().first shouldBe bigInt
val bigIntVarint = bigInt.toAsn1VarInt()
val rnd = Random.nextBytes(33)
val src = bigIntVarint.asList() + rnd
.asList()
src.decodeAsn1VarBigInt().first shouldBe bigInt

val buf = src.toByteArray().asBuffer()
buf.decodeAsn1VarBigInt().first shouldBe bigInt
rnd.forEach { it shouldBe buf.readByte() }
buf.exhausted().shouldBeTrue()
}
}
}
Expand Down

0 comments on commit a6bc66f

Please sign in to comment.