Skip to content

Commit ec68aff

Browse files
committed
Infer ByteArray size on encoding
1 parent 975d1e1 commit ec68aff

File tree

5 files changed

+192
-86
lines changed

5 files changed

+192
-86
lines changed

bluetooth/src/commonMain/kotlin/serialization/BinaryBuilder.kt

Lines changed: 99 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,21 @@ import com.splendo.kaluga.base.bytes.ByteOrder
2222
import com.splendo.kaluga.base.bytes.buildByteArray
2323
import com.splendo.kaluga.base.bytes.toByteArray
2424
import kotlinx.serialization.SerializationException
25+
import kotlin.math.ceil
2526

2627
/**
2728
* Builder for creating a [ByteArray] from a [BluetoothBinaryDescriptor]
2829
*/
2930
internal interface BinaryBuilder {
31+
32+
val expectedSize: Int
33+
3034
fun addFlag(index: Int, value: Boolean)
31-
fun addAction(action: ByteArrayBuilder.() -> Unit)
35+
fun addBit(value: Boolean)
36+
fun addAction(expectedSize: Int, action: ByteArrayBuilder.() -> Unit)
3237
fun makeUnconstrained()
3338

34-
fun build(): ByteArray
39+
fun ByteArrayBuilder.build()
3540
}
3641

3742
/**
@@ -46,17 +51,44 @@ class DataAfterUnconstrainedData(override val message: String?) : SerializationE
4651
internal abstract class StructureBinaryBuilder(val binaryDescriptor: BluetoothBinaryDescriptor, flagBitsSize: Int, private val onUnconstrained: () -> Unit) : BinaryBuilder {
4752

4853
val flagBits: MutableList<Boolean> = MutableList(flagBitsSize.coerceAtLeast(0)) { false }
54+
55+
override val expectedSize: Int
56+
get() = totalBodySize +
57+
(binaryDescriptor.structureSettings.checksumAlgorithm?.byteWidth ?: 0) +
58+
(binaryDescriptor.structureSettings.prefix?.array?.size ?: 0) +
59+
(binaryDescriptor.structureSettings.postfix?.array?.size ?: 0)
60+
private var currentBits: Int = flagBitsSize % Byte.SIZE_BITS
61+
private var expectedBodySize: Int = flagBitsSize / Byte.SIZE_BITS
62+
63+
private val totalBodySize: Int get() = expectedBodySize +
64+
if (currentBits > 0) 1 else 0
4965
private val actions = mutableListOf<ByteArrayBuilder.() -> Unit>()
5066
private var isOfUnconstrainedSize: Boolean = false
5167

5268
override fun addFlag(index: Int, value: Boolean) {
5369
flagBits[index] = value
5470
}
5571

56-
override fun addAction(action: ByteArrayBuilder.() -> Unit) {
72+
override fun addBit(value: Boolean) {
73+
currentBits++
74+
if (currentBits == Byte.SIZE_BITS) {
75+
expectedBodySize++
76+
currentBits = 0
77+
}
78+
actions += {
79+
add(value)
80+
}
81+
}
82+
83+
override fun addAction(expectedSize: Int, action: ByteArrayBuilder.() -> Unit) {
5784
if (isOfUnconstrainedSize) {
5885
throw DataAfterUnconstrainedData("Attempted to add data after data of an unconstrained size")
5986
}
87+
if (currentBits > 0) {
88+
expectedBodySize++
89+
currentBits = 0
90+
}
91+
expectedBodySize += expectedSize
6092
actions += action
6193
}
6294

@@ -65,24 +97,18 @@ internal abstract class StructureBinaryBuilder(val binaryDescriptor: BluetoothBi
6597
isOfUnconstrainedSize = true
6698
}
6799

68-
override fun build(): ByteArray {
69-
// The body is the flag bits + remaining body. This is also the part used for checksum verification
70-
val body = buildByteArray(binaryDescriptor.byteOrder, binaryDescriptor.expectedSize) {
71-
flagBits.forEach {
72-
add(it)
73-
}
74-
actions.forEach { apply(it) }
75-
}
76-
val crc = binaryDescriptor.structureSettings.checksumAlgorithm
77-
val additionalBytes = listOfNotNull(binaryDescriptor.structureSettings.prefix, binaryDescriptor.structureSettings.postfix).sumOf { it.array.size }
78-
79-
// Full data consists of prefix + body + checksum + postfix
80-
return buildByteArray(binaryDescriptor.byteOrder, binaryDescriptor.expectedSize + (crc?.byteWidth ?: 0) + additionalBytes) {
100+
override fun ByteArrayBuilder.build() {
101+
if (byteOrder == binaryDescriptor.byteOrder) {
81102
binaryDescriptor.structureSettings.prefix?.let {
82103
add(it.array)
83104
}
84-
add(body)
85-
crc?.let {
105+
val crc = binaryDescriptor.structureSettings.checksumAlgorithm
106+
if (crc != null) {
107+
// The body is the flag bits + remaining body. This is also the part used for checksum verification
108+
val body = buildByteArray(binaryDescriptor.byteOrder, totalBodySize) {
109+
buildBody()
110+
}
111+
add(body)
86112
// Calculate checksum if necessary
87113
val crcBytes = crc.compute(body).toByteArray(ByteOrder.LEAST_SIGNIFICANT_FIRST)
88114
for (i in 0..<crc.byteWidth) {
@@ -91,10 +117,31 @@ internal abstract class StructureBinaryBuilder(val binaryDescriptor: BluetoothBi
91117
ByteOrder.LEAST_SIGNIFICANT_FIRST -> add(crcBytes[i])
92118
}
93119
}
120+
} else {
121+
buildBody()
94122
}
123+
95124
binaryDescriptor.structureSettings.postfix?.let {
96125
add(it.array)
97126
}
127+
} else {
128+
add(
129+
buildByteArray(binaryDescriptor.byteOrder, expectedSize) {
130+
build()
131+
},
132+
)
133+
}
134+
}
135+
136+
private fun ByteArrayBuilder.buildBody() {
137+
flagBits.forEach {
138+
add(it)
139+
}
140+
actions.forEach { apply(it) }
141+
if (currentBits > 0) {
142+
repeat(Byte.SIZE_BITS - currentBits) {
143+
add(false)
144+
}
98145
}
99146
}
100147
}
@@ -135,22 +182,24 @@ class UnexpectedNullTermination(override val message: String) : SerializationExc
135182
/**
136183
* A [BinaryBuilder] to build data for a collection (List/Map) structure
137184
*/
138-
internal abstract class CollectionBinaryBuilder(
139-
private val byteOrder: ByteOrder,
140-
private val classBuilders: List<ItemBinaryBuilder>,
141-
private val expectedSize: Int,
142-
private val isNullTerminated: Boolean,
143-
) : BinaryBuilder {
185+
internal abstract class CollectionBinaryBuilder(private val byteOrder: ByteOrder, private val classBuilders: List<ItemBinaryBuilder>, private val isNullTerminated: Boolean) :
186+
BinaryBuilder {
144187
private var currentIndex = 0
145188
val currentClassBuilder: ItemBinaryBuilder get() = classBuilders[currentIndex]
146189

190+
override val expectedSize: Int get() = classBuilders.sumOf { it.expectedSize }
191+
147192
fun setIndex(index: Int): BluetoothBinaryDescriptor {
148193
currentIndex = index
149194
return currentClassBuilder.binaryDescriptor
150195
}
151196

152-
override fun addAction(action: ByteArrayBuilder.() -> Unit) {
153-
currentClassBuilder.addAction(action)
197+
override fun addBit(value: Boolean) {
198+
currentClassBuilder.addBit(value)
199+
}
200+
201+
override fun addAction(expectedSize: Int, action: ByteArrayBuilder.() -> Unit) {
202+
currentClassBuilder.addAction(expectedSize, action)
154203
}
155204

156205
override fun addFlag(index: Int, value: Boolean) {
@@ -161,19 +210,33 @@ internal abstract class CollectionBinaryBuilder(
161210
currentClassBuilder.makeUnconstrained()
162211
}
163212

164-
override fun build(): ByteArray = if (classBuilders.isEmpty()) {
165-
byteArrayOf()
166-
} else {
167-
buildByteArray(byteOrder, expectedSize) {
213+
override fun ByteArrayBuilder.build() {
214+
if (byteOrder == this@CollectionBinaryBuilder.byteOrder) {
168215
classBuilders.forEachIndexed { index, classBuilder ->
169-
val value = classBuilder.build()
170-
171-
// Ensure no unexpected null termination occurs
172-
if (isNullTerminated && classBuilder.binaryDescriptor.fieldIndex == 0 && classBuilder.checkIfStartsWithNull(value, byteOrder)) {
173-
throw UnexpectedNullTermination("The element at $index starts with Null Byte in a Null Terminated List")
216+
if (isNullTerminated && classBuilder.binaryDescriptor.fieldIndex == 0) {
217+
val value = buildByteArray(expectedSize = classBuilder.expectedSize) {
218+
with(classBuilder) {
219+
build()
220+
}
221+
}
222+
if (classBuilder.checkIfStartsWithNull(value, byteOrder)) {
223+
throw UnexpectedNullTermination("The element at $index starts with Null Byte in a Null Terminated List")
224+
}
225+
add(value)
226+
} else {
227+
with(classBuilder) {
228+
build()
229+
}
174230
}
175-
add(value)
176231
}
232+
} else {
233+
add(
234+
with(this@CollectionBinaryBuilder) {
235+
buildByteArray(byteOrder, expectedSize) {
236+
build()
237+
}
238+
},
239+
)
177240
}
178241
}
179242
}
@@ -187,7 +250,6 @@ internal class ListBinaryBuilder(binaryDescriptor: BluetoothBinaryDescriptor, si
187250
MutableList(size) {
188251
ItemBinaryBuilder(binaryDescriptor.children.first(), onUnconstrained)
189252
},
190-
binaryDescriptor.children.first().expectedSize * size,
191253
isNullTerminated,
192254
)
193255

@@ -201,6 +263,5 @@ internal class MapBinaryBuilder(binaryDescriptor: BluetoothBinaryDescriptor, siz
201263
val index = it % 2
202264
ItemBinaryBuilder(binaryDescriptor.children[index], onUnconstrained)
203265
},
204-
(binaryDescriptor.children[0].expectedSize + binaryDescriptor.children[1].expectedSize) * size,
205266
isNullTerminated,
206267
)

bluetooth/src/commonMain/kotlin/serialization/BluetoothBinaryDescriptorRegistry.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ internal data class BluetoothBinaryDescriptor(
4242
val bitWidth: Int,
4343
val byteOrder: ByteOrder,
4444
val isNullable: Boolean,
45-
val expectedSize: Int,
4645
val numericSettings: NumericSettings?,
4746
val stringSettings: StringSettings?,
4847
val collectionSettings: CollectionSettings?,
@@ -241,7 +240,6 @@ internal object BluetoothBinaryDescriptorRegistry {
241240

242241
// Nullable elements will have a flag bit. This is always the first bit of the flags for this object
243242
val isNullable = isNullable || (descriptor.kind in setOf(StructureKind.LIST, StructureKind.MAP) && annotations.filterIsInstance<NullIfEmpty>().isNotEmpty())
244-
val expectedSize = annotations.filterIsInstance<ExpectedSize>().firstOrNull()?.size ?: Long.SIZE_BYTES
245243
if (isNullable) {
246244
desiredFlagBitWidth.raise(1)
247245
}
@@ -280,7 +278,6 @@ internal object BluetoothBinaryDescriptorRegistry {
280278
width,
281279
byteOrder,
282280
isNullable,
283-
expectedSize,
284281
numericSettings,
285282
stringSettings,
286283
collectionSettings,

0 commit comments

Comments
 (0)