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()
 | 
			
		||||
            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")
 | 
			
		||||
 | 
			
		||||
@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user