diff --git a/src/commonMain/kotlin/net.sergeych.bintools/CRC.kt b/src/commonMain/kotlin/net.sergeych.bintools/CRC.kt new file mode 100644 index 0000000..5b5a16e --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.bintools/CRC.kt @@ -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 { + val lookupTable: List + 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 \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.bintools/CRC16.kt b/src/commonMain/kotlin/net.sergeych.bintools/CRC16.kt new file mode 100644 index 0000000..8ea72bd --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.bintools/CRC16.kt @@ -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 { + override val lookupTable: List = (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 + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.bintools/CRC32.kt b/src/commonMain/kotlin/net.sergeych.bintools/CRC32.kt new file mode 100644 index 0000000..14402aa --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.bintools/CRC32.kt @@ -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 { + override val lookupTable: List = (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 + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.bintools/CRC8.kt b/src/commonMain/kotlin/net.sergeych.bintools/CRC8.kt new file mode 100644 index 0000000..31e044c --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.bintools/CRC8.kt @@ -0,0 +1,35 @@ +package net.sergeych.bintools + +class CRC8(val polynomial: UByte = 0xA7.toUByte()) : CRC { + override val lookupTable: List = (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 + } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.bintools/DataSink.kt b/src/commonMain/kotlin/net.sergeych.bintools/DataSink.kt index 7c93b29..6db314f 100644 --- a/src/commonMain/kotlin/net.sergeych.bintools/DataSink.kt +++ b/src/commonMain/kotlin/net.sergeych.bintools/DataSink.kt @@ -35,6 +35,9 @@ inline fun 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)) diff --git a/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt b/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt index 3e9b948..2c2ea5b 100644 --- a/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt +++ b/src/commonMain/kotlin/net.sergeych.bintools/DataSource.kt @@ -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)) diff --git a/src/commonMain/kotlin/net.sergeych.bintools/simple_codecs.kt b/src/commonMain/kotlin/net.sergeych.bintools/simple_codecs.kt index c2f3c1b..656d481 100644 --- a/src/commonMain/kotlin/net.sergeych.bintools/simple_codecs.kt +++ b/src/commonMain/kotlin/net.sergeych.bintools/simple_codecs.kt @@ -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" diff --git a/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt b/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt index 32b47d9..ba1eaab 100644 --- a/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt +++ b/src/commonMain/kotlin/net.sergeych.bipack/BipackDecoder.kt @@ -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().toInt() - else descriptor.elementsCount - ) + var count = descriptor.elementsCount + for( a in descriptor.annotations ) { + if( a is ExtendableFormat ) + count = input.readNumber().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 = diff --git a/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt b/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt index 6ae0691..2dd1b2b 100644 --- a/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt +++ b/src/commonMain/kotlin/net.sergeych.bipack/BipackEncoder.kt @@ -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) } diff --git a/src/commonMain/kotlin/net.sergeych.bipack/Framed.kt b/src/commonMain/kotlin/net.sergeych.bipack/Framed.kt new file mode 100644 index 0000000..43a9525 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.bipack/Framed.kt @@ -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") diff --git a/src/jvmTest/kotlin/net/sergeych/bipack/BipackEncoderTest.kt b/src/jvmTest/kotlin/net/sergeych/bipack/BipackEncoderTest.kt index 3e97311..640d916 100644 --- a/src/jvmTest/kotlin/net/sergeych/bipack/BipackEncoderTest.kt +++ b/src/jvmTest/kotlin/net/sergeych/bipack/BipackEncoderTest.kt @@ -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(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(BipackEncoder.encode(a)) + assertEquals(a, b) + val c = BipackDecoder.decode(BipackEncoder.encode(a)) + assertEquals(-1, c.other) + assertEquals(a.bar, c.bar) + assertThrows(InvalidFrameException::class.java) { + BipackDecoder.decode(BipackEncoder.encode(a)) + } + } + + @Test + fun encodeNonExtendable() { + val a = Foobar1N(42)//, "bum") + println(BipackEncoder.encode(a).toDump()) + val b = BipackDecoder.decode(BipackEncoder.encode(a)) + assertEquals(a, b) } } \ No newline at end of file