support for serialized frames
This commit is contained in:
parent
63b231975b
commit
be03163a96
51
src/commonMain/kotlin/net.sergeych.bintools/CRC.kt
Normal file
51
src/commonMain/kotlin/net.sergeych.bintools/CRC.kt
Normal file
@ -0,0 +1,51 @@
|
||||
@file:OptIn(ExperimentalUnsignedTypes::class)
|
||||
|
||||
package net.sergeych.bintools
|
||||
|
||||
/**
|
||||
* Common interfacte for all CRC variants. See [crc16], [crc32] and [crc8] for useful shortcuts.
|
||||
*/
|
||||
interface CRC<T> {
|
||||
val lookupTable: List<T>
|
||||
val value: T
|
||||
|
||||
fun update(inputs: UByteArray)
|
||||
fun reset()
|
||||
|
||||
fun update(input: UByte) {
|
||||
update(ubyteArrayOf(input))
|
||||
}
|
||||
|
||||
fun update(inputs: ByteArray) {
|
||||
update(inputs.toUByteArray())
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Calculate crc8 for a data array using a given polynomial (uses CRC8-Bluetooth's polynomial by default)
|
||||
*/
|
||||
fun crc8(data: ByteArray, polynomial: UByte = 0xA7.toUByte()): UByte =
|
||||
CRC8(polynomial).also { it.update(data) }.value
|
||||
|
||||
/**
|
||||
* Calculate CRC16 for a data array using a given polynomial (CRC16-CCITT polynomial (0x1021) by default)
|
||||
*/
|
||||
fun crc16(data: ByteArray, polynomial: UShort = 0x1021.toUShort()): UShort =
|
||||
CRC16(polynomial).also { it.update(data) }.value
|
||||
|
||||
/**
|
||||
* Calculate crc32 for a given data and polynomial (using CRC32 polynomial by default)
|
||||
*/
|
||||
fun crc32(data: ByteArray, polynomial: UInt = 0x04C11DB7.toUInt()): UInt =
|
||||
CRC32(polynomial).also { it.update(data) }.value
|
||||
}
|
||||
}
|
||||
|
||||
infix fun UShort.shl(bitCount: Int): UShort = (this.toUInt() shl bitCount).toUShort()
|
||||
infix fun UShort.shr(bitCount: Int): UShort = (this.toUInt() shr bitCount).toUShort()
|
||||
|
||||
infix fun UByte.shl(bitCount: Int): UByte = (this.toUInt() shl bitCount).toUByte()
|
||||
infix fun UByte.shr(bitCount: Int): UByte = (this.toUInt() shr bitCount).toUByte()
|
||||
|
||||
fun UByte.toBigEndianUShort(): UShort = this.toUShort() shl 8
|
||||
fun UByte.toBigEndianUInt(): UInt = this.toUInt() shl 24
|
43
src/commonMain/kotlin/net.sergeych.bintools/CRC16.kt
Normal file
43
src/commonMain/kotlin/net.sergeych.bintools/CRC16.kt
Normal file
@ -0,0 +1,43 @@
|
||||
@file:OptIn(ExperimentalUnsignedTypes::class)
|
||||
|
||||
package net.sergeych.bintools
|
||||
|
||||
/**
|
||||
* Class to conveniently calculate CRC-16. It uses the CRC16-CCITT polynomial (0x1021) by default
|
||||
*/
|
||||
class CRC16(val polynomial: UShort = 0x1021.toUShort()) : CRC<UShort> {
|
||||
override val lookupTable: List<UShort> = (0 until 256).map { crc16(it.toUByte(), polynomial) }
|
||||
|
||||
override var value: UShort = 0.toUShort()
|
||||
private set
|
||||
|
||||
override fun update(inputs: UByteArray) {
|
||||
value = crc16(inputs, value)
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
value = 0.toUShort()
|
||||
}
|
||||
|
||||
private fun crc16(inputs: UByteArray, initialValue: UShort = 0.toUShort()): UShort {
|
||||
return inputs.fold(initialValue) { remainder, byte ->
|
||||
val bigEndianInput = byte.toBigEndianUShort()
|
||||
val index = (bigEndianInput xor remainder) shr 8
|
||||
lookupTable[index.toInt()] xor (remainder shl 8)
|
||||
}
|
||||
}
|
||||
|
||||
private fun crc16(input: UByte, polynomial: UShort): UShort {
|
||||
val bigEndianInput = input.toBigEndianUShort()
|
||||
|
||||
return (0 until 8).fold(bigEndianInput) { result, _ ->
|
||||
val isMostSignificantBitOne = result and 0x8000.toUShort() != 0.toUShort()
|
||||
val shiftedResult = result shl 1
|
||||
|
||||
when (isMostSignificantBitOne) {
|
||||
true -> shiftedResult xor polynomial
|
||||
false -> shiftedResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
src/commonMain/kotlin/net.sergeych.bintools/CRC32.kt
Normal file
43
src/commonMain/kotlin/net.sergeych.bintools/CRC32.kt
Normal file
@ -0,0 +1,43 @@
|
||||
@file:OptIn(ExperimentalUnsignedTypes::class)
|
||||
|
||||
package net.sergeych.bintools
|
||||
|
||||
/**
|
||||
* Class to conveniently calculate CRC-32 using CRC32 polynomial (0x04C11DB7) by default
|
||||
*/
|
||||
class CRC32(val polynomial: UInt = 0x04C11DB7.toUInt()) : CRC<UInt> {
|
||||
override val lookupTable: List<UInt> = (0 until 256).map { crc32(it.toUByte(), polynomial) }
|
||||
|
||||
override var value: UInt = 0.toUInt()
|
||||
private set
|
||||
|
||||
override fun update(inputs: UByteArray) {
|
||||
value = crc32(inputs, value)
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
value = 0.toUInt()
|
||||
}
|
||||
|
||||
private fun crc32(inputs: UByteArray, initialValue: UInt = 0.toUInt()): UInt {
|
||||
return inputs.fold(initialValue) { remainder, byte ->
|
||||
val bigEndianInput = byte.toBigEndianUInt()
|
||||
val index = (bigEndianInput xor remainder) shr 24
|
||||
lookupTable[index.toInt()] xor (remainder shl 8)
|
||||
}
|
||||
}
|
||||
|
||||
private fun crc32(input: UByte, polynomial: UInt): UInt {
|
||||
val bigEndianInput = input.toBigEndianUInt()
|
||||
|
||||
return (0 until 8).fold(bigEndianInput) { result, _ ->
|
||||
val isMostSignificantBitOne = result and 0x80000000.toUInt() != 0.toUInt()
|
||||
val shiftedResult = result shl 1
|
||||
|
||||
when (isMostSignificantBitOne) {
|
||||
true -> shiftedResult xor polynomial
|
||||
false -> shiftedResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
src/commonMain/kotlin/net.sergeych.bintools/CRC8.kt
Normal file
35
src/commonMain/kotlin/net.sergeych.bintools/CRC8.kt
Normal file
@ -0,0 +1,35 @@
|
||||
package net.sergeych.bintools
|
||||
|
||||
class CRC8(val polynomial: UByte = 0xA7.toUByte()) : CRC<UByte> {
|
||||
override val lookupTable: List<UByte> = (0 until 256).map { crc8(it.toUByte(), polynomial) }
|
||||
|
||||
override var value: UByte = 0.toUByte()
|
||||
private set
|
||||
|
||||
override fun update(inputs: UByteArray) {
|
||||
value = crc8(inputs, value)
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
value = 0.toUByte()
|
||||
}
|
||||
|
||||
private fun crc8(inputs: UByteArray, initialValue: UByte = 0.toUByte()): UByte {
|
||||
return inputs.fold(initialValue) { remainder, byte ->
|
||||
val index = byte xor remainder
|
||||
lookupTable[index.toInt()]
|
||||
}
|
||||
}
|
||||
|
||||
private fun crc8(input: UByte, polynomial: UByte): UByte {
|
||||
return (0 until 8).fold(input) { result, _ ->
|
||||
val isMostSignificantBitOne = result and 0x80.toUByte() != 0.toUByte()
|
||||
val shiftedResult = result shl 1
|
||||
|
||||
when (isMostSignificantBitOne) {
|
||||
true -> shiftedResult xor polynomial
|
||||
false -> shiftedResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,9 @@ inline fun <reified T:Any>DataSink.writeNumber(value: T) {
|
||||
fun DataSink.writeI32(value: Int) {
|
||||
writeBytes(intToBytes(value))
|
||||
}
|
||||
fun DataSink.writeU32(value: UInt) {
|
||||
writeBytes(uintToBytes(value))
|
||||
}
|
||||
|
||||
fun DataSink.writeI64(value: Long) {
|
||||
writeBytes(longToBytes(value))
|
||||
|
@ -24,6 +24,7 @@ interface DataSource {
|
||||
a[i] = readByte()
|
||||
}
|
||||
|
||||
fun readU32(): UInt = bytesToUInt(readBytes(4))
|
||||
fun readI32(): Int = bytesToInt(readBytes(4))
|
||||
fun readI64(): Long = bytesToLong(readBytes(8))
|
||||
|
||||
|
@ -21,6 +21,16 @@ fun intToBytes(value: Int): ByteArray {
|
||||
return result
|
||||
}
|
||||
|
||||
fun uintToBytes(value: UInt): ByteArray {
|
||||
var l = value
|
||||
val result = ByteArray(4)
|
||||
for (i in 3 downTo 0) {
|
||||
result[i] = (l and 0xFFu).toByte()
|
||||
l = l shr 8
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert 8 bytes to LE long
|
||||
*/
|
||||
@ -42,6 +52,15 @@ fun bytesToInt(b: ByteArray): Int {
|
||||
return result
|
||||
}
|
||||
|
||||
fun bytesToUInt(b: ByteArray): UInt {
|
||||
var result = 0u
|
||||
for (i in 0 until 4) {
|
||||
result = result shl 8
|
||||
result = result or (b[i].toUInt() and 0xFFu)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private val hexDigits = "0123456789ABCDEF"
|
||||
|
||||
|
@ -8,6 +8,7 @@ import kotlinx.serialization.encoding.CompositeDecoder
|
||||
import kotlinx.serialization.modules.EmptySerializersModule
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.serializer
|
||||
import net.sergeych.bintools.CRC
|
||||
import net.sergeych.bintools.DataSource
|
||||
import net.sergeych.bintools.readNumber
|
||||
import net.sergeych.bintools.toDataSource
|
||||
@ -39,12 +40,18 @@ class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : Abstrac
|
||||
}
|
||||
|
||||
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
|
||||
return BipackDecoder(
|
||||
input,
|
||||
if (descriptor.annotations.any { it is ExtendableFormat })
|
||||
input.readNumber<UInt>().toInt()
|
||||
else descriptor.elementsCount
|
||||
)
|
||||
var count = descriptor.elementsCount
|
||||
for( a in descriptor.annotations ) {
|
||||
if( a is ExtendableFormat )
|
||||
count = input.readNumber<UInt>().toInt()
|
||||
else if( a is Framed ) {
|
||||
val code = CRC.crc32(descriptor.serialName.encodeToByteArray())
|
||||
val actual = input.readU32()
|
||||
if( code != actual )
|
||||
throw InvalidFrameException()
|
||||
}
|
||||
}
|
||||
return BipackDecoder(input, count)
|
||||
}
|
||||
|
||||
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
|
||||
|
@ -7,10 +7,7 @@ import kotlinx.serialization.encoding.CompositeEncoder
|
||||
import kotlinx.serialization.modules.EmptySerializersModule
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.serializer
|
||||
import net.sergeych.bintools.ArrayDataSink
|
||||
import net.sergeych.bintools.DataSink
|
||||
import net.sergeych.bintools.writeI64
|
||||
import net.sergeych.bintools.writeNumber
|
||||
import net.sergeych.bintools.*
|
||||
|
||||
class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
||||
override val serializersModule: SerializersModule = EmptySerializersModule
|
||||
@ -42,8 +39,16 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
||||
}
|
||||
|
||||
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
|
||||
if( descriptor.annotations.any { it is ExtendableFormat } )
|
||||
encodeUInt(descriptor.elementsCount.toUInt())
|
||||
for( a in descriptor.annotations) {
|
||||
if (a is Framed) {
|
||||
output.writeU32(
|
||||
CRC.crc32(descriptor.serialName.encodeToByteArray())
|
||||
)
|
||||
}
|
||||
else if( a is ExtendableFormat) {
|
||||
encodeUInt(descriptor.elementsCount.toUInt())
|
||||
}
|
||||
}
|
||||
return super.beginStructure(descriptor)
|
||||
}
|
||||
|
||||
|
14
src/commonMain/kotlin/net.sergeych.bipack/Framed.kt
Normal file
14
src/commonMain/kotlin/net.sergeych.bipack/Framed.kt
Normal file
@ -0,0 +1,14 @@
|
||||
package net.sergeych.bipack
|
||||
|
||||
import kotlinx.serialization.SerialInfo
|
||||
|
||||
/**
|
||||
* Serializable classes annotated as Framed will have leading checked mark as CRC32
|
||||
* of its name (uses `@SerialName` internally). On deserializing, if frame will not
|
||||
* match the expected name, the [InvalidFrameException] will be thrown.
|
||||
*/
|
||||
@SerialInfo
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Framed
|
||||
|
||||
class InvalidFrameException : Exception("invalid CRC32 serialization frame value")
|
@ -1,17 +1,37 @@
|
||||
package net.sergeych.bipack
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.sergeych.bintools.toDump
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@Serializable
|
||||
data class Foobar1N(val bar: Int, val foo: Int = 117)
|
||||
|
||||
@Serializable
|
||||
@ExtendableFormat
|
||||
data class Foobar1(val bar: Int, val foo: Int = 117)
|
||||
|
||||
@Serializable
|
||||
@ExtendableFormat
|
||||
@SerialName("net.sergeych.bipack.Foobar1")
|
||||
data class Foobar2(val bar: Int, val foo: Int,val other: Int = -1)
|
||||
@Serializable
|
||||
@Framed
|
||||
@ExtendableFormat
|
||||
data class FoobarF1(val bar: Int, val foo: Int = 117)
|
||||
|
||||
@Serializable
|
||||
@Framed
|
||||
@ExtendableFormat
|
||||
@SerialName("net.sergeych.bipack.FoobarF1")
|
||||
data class FoobarF2(val bar: Int, val foo: Int,val other: Int = -1)
|
||||
@Serializable
|
||||
@Framed
|
||||
@ExtendableFormat
|
||||
data class FoobarF3(val bar: Int, val foo: Int,val other: Int = -1)
|
||||
|
||||
class BipackEncoderTest {
|
||||
|
||||
@ -23,8 +43,27 @@ class BipackEncoderTest {
|
||||
assertEquals(a, b)
|
||||
val c = BipackDecoder.decode<Foobar2>(BipackEncoder.encode(a))
|
||||
assertEquals(-1, c.other)
|
||||
// assertEquals(a.foo, c.foo)
|
||||
assertEquals(a.bar, c.bar)
|
||||
// assertEquals(a.buzz, c.buzz)
|
||||
}
|
||||
@Test
|
||||
fun encodeFramed() {
|
||||
val a = FoobarF1(42)//, "bum")
|
||||
println(BipackEncoder.encode(a).toDump())
|
||||
val b = BipackDecoder.decode<FoobarF1>(BipackEncoder.encode(a))
|
||||
assertEquals(a, b)
|
||||
val c = BipackDecoder.decode<FoobarF2>(BipackEncoder.encode(a))
|
||||
assertEquals(-1, c.other)
|
||||
assertEquals(a.bar, c.bar)
|
||||
assertThrows(InvalidFrameException::class.java) {
|
||||
BipackDecoder.decode<FoobarF3>(BipackEncoder.encode(a))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodeNonExtendable() {
|
||||
val a = Foobar1N(42)//, "bum")
|
||||
println(BipackEncoder.encode(a).toDump())
|
||||
val b = BipackDecoder.decode<Foobar1N>(BipackEncoder.encode(a))
|
||||
assertEquals(a, b)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user