CRC-protected frames

This commit is contained in:
sergeych committed 2023-03-13 15:28:08 +01:00
1 parent ee2c958445
commit 8c73ec9e30
8 files changed
+155 -27

No files matched your search

@@ -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
}
@@ -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()
else if( a is Framed ) {
for (a in descriptor.annotations) {
if (a is ExtendableFormat)
count = source.readNumber<UInt>().toInt()
else if (a is Framed) {
val code = CRC.crc32(descriptor.serialName.encodeToByteArray())
val actual = input.readU32()
if( code != actual )
throw InvalidFrameException()
// 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 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")