CRC-protected frames
This commit is contained in:
parent
ee2c958445
commit
8c73ec9e30
26
src/commonMain/kotlin/net.sergeych.bintools/CRC32Sink.kt
Normal file
26
src/commonMain/kotlin/net.sergeych.bintools/CRC32Sink.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package net.sergeych.bintools
|
||||
|
||||
/**
|
||||
* The filter-type sink that calculates CRC32 on the fly passing data to
|
||||
* another sink
|
||||
*/
|
||||
class CRC32Sink(val sink: DataSink,
|
||||
polynomial:UInt = 0x04C11DB7U): DataSink {
|
||||
|
||||
private val checksum = CRC32(polynomial)
|
||||
override fun writeByte(data: Byte) {
|
||||
checksum.update(data.toUByte())
|
||||
sink.writeByte(data)
|
||||
}
|
||||
|
||||
override fun writeBytes(data: ByteArray) {
|
||||
checksum.update(data.toUByteArray())
|
||||
sink.writeBytes(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* current value of the CRC32 checkcum, using the bytes that
|
||||
* were written by now.
|
||||
*/
|
||||
val crc: UInt get() = checksum.value
|
||||
}
|
18
src/commonMain/kotlin/net.sergeych.bintools/CRC32Source.kt
Normal file
18
src/commonMain/kotlin/net.sergeych.bintools/CRC32Source.kt
Normal file
@ -0,0 +1,18 @@
|
||||
package net.sergeych.bintools
|
||||
|
||||
class CRC32Source(
|
||||
val source: DataSource,
|
||||
polynomial: UInt = 0x04C11DB7U
|
||||
): DataSource {
|
||||
|
||||
private val checksum = CRC32(polynomial)
|
||||
override fun readByte(): Byte = source.readByte().also {
|
||||
checksum.update(it.toUByte())
|
||||
}
|
||||
|
||||
override fun readBytes(size: Int): ByteArray = source.readBytes(size).also {
|
||||
checksum.update(it.toUByteArray())
|
||||
}
|
||||
|
||||
val crc: UInt get() = checksum.value
|
||||
}
|
@ -10,6 +10,9 @@ import kotlin.reflect.typeOf
|
||||
*/
|
||||
interface DataSource {
|
||||
|
||||
/**
|
||||
* Exception that implementations must throw on end of data
|
||||
*/
|
||||
class EndOfData() : Exception("no more data available")
|
||||
|
||||
fun readByte(): Byte
|
||||
|
@ -8,11 +8,12 @@ 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
|
||||
import net.sergeych.bintools.*
|
||||
|
||||
/**
|
||||
* Decode BiPack format. Note that it relies on [DataSource] so can throw [DataSource.EndOfData]
|
||||
* excpetion. Specific frames when used can throw [InvalidFrameException] and its derivatives.e
|
||||
*/
|
||||
class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : AbstractDecoder() {
|
||||
private var elementIndex = 0
|
||||
|
||||
@ -40,18 +41,37 @@ class BipackDecoder(val input: DataSource, var elementsCount: Int = 0) : Abstrac
|
||||
}
|
||||
|
||||
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
|
||||
var source = if (descriptor.annotations.any { it is CrcProtected })
|
||||
CRC32Source(input)
|
||||
else
|
||||
input
|
||||
|
||||
// Note: we should read from 'source' explicitely as it might ve
|
||||
// CRC-calculating one, and the fields below are CRC protected too:
|
||||
var count = descriptor.elementsCount
|
||||
for (a in descriptor.annotations) {
|
||||
if (a is ExtendableFormat)
|
||||
count = input.readNumber<UInt>().toInt()
|
||||
count = source.readNumber<UInt>().toInt()
|
||||
else if (a is Framed) {
|
||||
val code = CRC.crc32(descriptor.serialName.encodeToByteArray())
|
||||
val actual = input.readU32()
|
||||
// if we fail to read CRC, it is IO error, so DataSource.EndOfData will be
|
||||
// thrown here, and it is better than invalid frame exception:
|
||||
val actual = source.readU32()
|
||||
if (code != actual)
|
||||
throw InvalidFrameException()
|
||||
throw InvalidFrameHeaderException()
|
||||
}
|
||||
}
|
||||
return BipackDecoder(input, count)
|
||||
return BipackDecoder(source, count)
|
||||
}
|
||||
|
||||
override fun endStructure(descriptor: SerialDescriptor) {
|
||||
if (input is CRC32Source) {
|
||||
val actual = input.crc
|
||||
val expected = input.readU32()
|
||||
if (actual != expected)
|
||||
throw InvalidFrameCRCException()
|
||||
}
|
||||
super.endStructure(descriptor)
|
||||
}
|
||||
|
||||
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int =
|
||||
|
@ -9,7 +9,11 @@ import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.serializer
|
||||
import net.sergeych.bintools.*
|
||||
|
||||
class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
||||
class BipackEncoder(var output: DataSink) : AbstractEncoder() {
|
||||
|
||||
// used when CRC calculation on the fly
|
||||
private var crcSink: CRC32Sink? = null
|
||||
|
||||
override val serializersModule: SerializersModule = EmptySerializersModule
|
||||
override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
|
||||
override fun encodeByte(value: Byte) = output.writeByte(value.toInt())
|
||||
@ -26,6 +30,10 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
||||
writeBytes(value.encodeToByteArray())
|
||||
}
|
||||
|
||||
override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean {
|
||||
return super.encodeElement(descriptor, index)
|
||||
}
|
||||
|
||||
fun writeBytes(value: ByteArray) {
|
||||
output.writeNumber(value.size.toUInt())
|
||||
output.writeBytes(value)
|
||||
@ -39,6 +47,13 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
||||
}
|
||||
|
||||
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
|
||||
// frame protection should start before anything else:
|
||||
if( descriptor.annotations.any { it is CrcProtected }) {
|
||||
crcSink = CRC32Sink(output).also {
|
||||
output = it
|
||||
}
|
||||
}
|
||||
// now it is safe to process anything else
|
||||
for( a in descriptor.annotations) {
|
||||
if (a is Framed) {
|
||||
output.writeU32(
|
||||
@ -52,6 +67,15 @@ class BipackEncoder(val output: DataSink) : AbstractEncoder() {
|
||||
return super.beginStructure(descriptor)
|
||||
}
|
||||
|
||||
override fun endStructure(descriptor: SerialDescriptor) {
|
||||
crcSink?.let {
|
||||
output = it.sink
|
||||
crcSink = null
|
||||
output.writeU32(it.crc)
|
||||
}
|
||||
super.endStructure(descriptor)
|
||||
}
|
||||
|
||||
override fun encodeNull() = encodeBoolean(false)
|
||||
override fun encodeNotNullMark() = encodeBoolean(true)
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
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")
|
@ -16,3 +16,22 @@ import kotlinx.serialization.SerialInfo
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@SerialInfo
|
||||
annotation class ExtendableFormat
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@SerialInfo
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class CrcProtected
|
||||
|
||||
open class InvalidFrameException(reason: String) : Exception(reason)
|
||||
class InvalidFrameHeaderException(reason: String = "Frame header does not match") : InvalidFrameException(reason)
|
||||
class InvalidFrameCRCException : InvalidFrameException("Checksum CRC32 failed")
|
||||
|
@ -4,6 +4,7 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import net.sergeych.bintools.toDump
|
||||
import net.sergeych.bipack.*
|
||||
import kotlin.experimental.xor
|
||||
import kotlin.test.*
|
||||
|
||||
@Serializable
|
||||
@ -31,6 +32,12 @@ data class FoobarF2(val bar: Int, val foo: Int,val other: Int = -1)
|
||||
@ExtendableFormat
|
||||
data class FoobarF3(val bar: Int, val foo: Int,val other: Int = -1)
|
||||
|
||||
@Serializable
|
||||
@Framed
|
||||
@ExtendableFormat
|
||||
@CrcProtected()
|
||||
data class FoobarFP1(val bar: Int, val foo: Int,val other: Int = -1)
|
||||
|
||||
class BipackEncoderTest {
|
||||
|
||||
@Test
|
||||
@ -64,4 +71,29 @@ class BipackEncoderTest {
|
||||
val b = BipackDecoder.decode<Foobar1N>(BipackEncoder.encode(a))
|
||||
assertEquals(a, b)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodeProtectedFrame() {
|
||||
val a = FoobarFP1(42, 117)//, "bum")
|
||||
println(BipackEncoder.encode(a).toDump())
|
||||
val b = BipackDecoder.decode<FoobarFP1>(BipackEncoder.encode(a))
|
||||
assertEquals(a, b)
|
||||
val bad = BipackEncoder.encode(a)
|
||||
bad[6] = bad[6] xor 0x20
|
||||
println(bad.toDump())
|
||||
assertFailsWith(InvalidFrameCRCException::class) {
|
||||
BipackDecoder.decode<FoobarFP1>(bad)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class FBU(val u: UInt, val i: Int)
|
||||
@Test
|
||||
fun testByteArray() {
|
||||
val x = byteArrayOf(1,2,3)
|
||||
println(BipackEncoder.encode(x).toDump())
|
||||
println(BipackEncoder.encode("123").toDump())
|
||||
println(BipackEncoder.encode(FBU(3u, 3)).toDump())
|
||||
// println(BipackEncoder.encode(1U).toDump())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user