Skip to content

Commit 34a3754

Browse files
committed
Add struct packing/unpacking utility class
1 parent a8710ae commit 34a3754

File tree

3 files changed

+281
-0
lines changed

3 files changed

+281
-0
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (c) Kuba Szczodrzyński 2022-12-19.
3+
*/
4+
5+
package io.github.cloudcutter.work.common
6+
7+
abstract class ByteStruct {
8+
9+
abstract fun getValues(): List<Any>
10+
11+
fun getFormat(): String =
12+
this::class.java.getAnnotation(ByteStructFormat::class.java)!!.format
13+
14+
companion object {
15+
inline fun <reified T : ByteStruct> unpack(data: ByteArray, mode: Int = 0) =
16+
unpackImpl(data, T::class.java, mode)
17+
18+
fun pack(data: ByteStruct, mode: Int = 0): ByteArray {
19+
val cls = data::class.java
20+
// retrieve child type annotations
21+
val childTypeMap = cls.getAnnotation(ByteStructChildMap::class.java)?.map?.filter {
22+
it.mode == mode
23+
}
24+
25+
// retrieve format string and get struct values
26+
val formatString = cls.getAnnotation(ByteStructFormat::class.java)!!.format
27+
val values = data.getValues().toMutableList()
28+
29+
// update child type fields
30+
for ((index, value) in values.withIndex()) {
31+
if (value is String) {
32+
values[index] = value.toByteArray()
33+
continue
34+
}
35+
if (value !is ByteStruct)
36+
continue
37+
values[index] = pack(value)
38+
val annotation = childTypeMap?.firstOrNull {
39+
index == it.dataFieldIndex && value::class == it.dataFieldClass
40+
} ?: continue
41+
values[annotation.typeFieldIndex] = annotation.typeFieldValue
42+
}
43+
44+
return Struct.pack(formatString, values)
45+
}
46+
47+
fun <T : ByteStruct> unpackImpl(data: ByteArray, cls: Class<T>, mode: Int = 0): T {
48+
// retrieve child type annotations
49+
val childTypeMap = cls.getAnnotation(ByteStructChildMap::class.java)?.map?.filter {
50+
it.mode == mode
51+
}
52+
53+
// retrieve format string and unpack the struct
54+
val formatString = cls.getAnnotation(ByteStructFormat::class.java)!!.format
55+
val values = Struct.unpack(formatString, data).toMutableList()
56+
57+
// deserialize all child ByteStruct fields
58+
val constructor = cls.constructors.first()
59+
for ((index, type) in constructor.parameterTypes.withIndex()) {
60+
if (String::class.java.isAssignableFrom(type)) {
61+
values[index] = (values[index] as ByteArray).decodeToString()
62+
continue
63+
}
64+
if (!ByteStruct::class.java.isAssignableFrom(type))
65+
continue
66+
val annotation = childTypeMap?.firstOrNull {
67+
index == it.dataFieldIndex && values[it.typeFieldIndex] == it.typeFieldValue
68+
}
69+
val childType = annotation?.dataFieldClass?.java ?: type
70+
values[index] = unpackImpl(values[index] as ByteArray, childType as Class<T>)
71+
}
72+
73+
return constructor.newInstance(*values.toTypedArray()) as T
74+
}
75+
}
76+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (c) Kuba Szczodrzyński 2022-12-19.
3+
*/
4+
5+
package io.github.cloudcutter.work.common
6+
7+
import kotlin.reflect.KClass
8+
9+
annotation class ByteStructFormat(
10+
val format: String,
11+
)
12+
13+
annotation class ByteStructChild(
14+
val typeFieldIndex: Int,
15+
val typeFieldValue: Int,
16+
val dataFieldIndex: Int,
17+
val dataFieldClass: KClass<out ByteStruct>,
18+
val mode: Int = 0,
19+
)
20+
21+
annotation class ByteStructChildMap(
22+
vararg val map: ByteStructChild,
23+
)
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright (c) Kuba Szczodrzyński 2022-12-19.
3+
*/
4+
5+
package io.github.cloudcutter.work.common
6+
7+
import java.nio.ByteBuffer
8+
import java.nio.ByteOrder
9+
10+
object Struct {
11+
12+
private fun String.getEndianness() = when (this[0]) {
13+
'<' -> ByteOrder.LITTLE_ENDIAN
14+
'>', '!' -> ByteOrder.BIG_ENDIAN
15+
else -> ByteOrder.nativeOrder()
16+
}
17+
18+
private fun String.getFormatList() = this
19+
.replace(" ", "")
20+
.replace("""(\d+[^s\d])""".toRegex()) {
21+
it.value
22+
.last()
23+
.toString()
24+
.repeat(it.value.dropLast(1).toInt())
25+
}
26+
.replace("""\$\d+s|\d+s""".toRegex()) { " ${it.value} " }
27+
.split(" ")
28+
.filter { it.isNotBlank() }
29+
.flatMap { if (it.last() == 's') listOf(it) else it.split("") }
30+
.map { if (it == "s") "1s" else it }
31+
.filter { it.isNotBlank() }
32+
.dropWhile { it[0] in "@=<>!" }
33+
34+
private fun getLengthFields(format: List<String>): Map<Int, Int> {
35+
// Map<length field, data field>
36+
val lengthFields = mutableMapOf<Int, Int>()
37+
38+
// find all dynamically-sized strings
39+
var nullCount = 0
40+
for ((index, field) in format.withIndex()) {
41+
if (field == "x") {
42+
nullCount++
43+
continue
44+
}
45+
if (!field.startsWith("$"))
46+
continue
47+
val fieldIndex = field.drop(1).dropLast(1).toInt()
48+
lengthFields[fieldIndex] = index - nullCount
49+
}
50+
return lengthFields
51+
}
52+
53+
fun pack(formatString: String, valuesRaw: List<Any>): ByteArray {
54+
val endianness = formatString.getEndianness()
55+
val format = formatString.getFormatList()
56+
val lengthFields = getLengthFields(format)
57+
58+
// replace 'length fields' with actual length
59+
val values = valuesRaw.mapIndexed { index, value ->
60+
if (index in lengthFields)
61+
(valuesRaw[lengthFields[index] ?: 0] as ByteArray).size
62+
else
63+
value
64+
}
65+
66+
var nullCount = 0
67+
val totalLength = format.mapIndexed { index, field ->
68+
if (field == "x")
69+
nullCount++
70+
when (field.last()) {
71+
'x' -> 1
72+
'c', 'b', 'B' -> 1
73+
'?' -> 1
74+
'h', 'H' -> 2
75+
'i', 'I' -> 4
76+
'l', 'L' -> 4
77+
'q', 'Q' -> 8
78+
'f' -> 4
79+
'd' -> 8
80+
's' -> if (field[0] == '$')
81+
(values[index - nullCount] as ByteArray).size
82+
else
83+
field.dropLast(1).toInt()
84+
'S' -> (values[index - nullCount] as ByteArray).size
85+
else -> throw UnsupportedOperationException("Format specifier $field not supported")
86+
}
87+
}.sum()
88+
89+
val buf = ByteBuffer.allocate(totalLength).order(endianness)
90+
nullCount = 0
91+
for ((index, field) in format.withIndex()) {
92+
val spec = field.last()
93+
if (spec == 'x') {
94+
nullCount++
95+
buf.put(0)
96+
continue
97+
}
98+
val value = values[index - nullCount]
99+
println("Packing field $index - $field($spec) - $value")
100+
when (spec) {
101+
'c' -> buf.putChar(value as Char)
102+
'b', 'B' -> buf.put((value as Number).toByte())
103+
'?' -> buf.put(if (value == true) 1 else 0)
104+
'h', 'H' -> buf.putShort((value as Number).toShort())
105+
'i', 'I' -> buf.putInt((value as Number).toInt())
106+
'l', 'L' -> buf.putInt((value as Number).toInt())
107+
'q', 'Q' -> buf.putLong((value as Number).toLong())
108+
'f' -> buf.putFloat((value as Number).toFloat())
109+
'd' -> buf.putDouble((value as Number).toDouble())
110+
's' -> {
111+
val size = (value as ByteArray).size
112+
if (field[0] == '$') {
113+
buf.put(value)
114+
} else {
115+
val fieldSize = field.dropLast(1).toInt()
116+
when {
117+
size > fieldSize -> buf.put(value.take(fieldSize).toByteArray())
118+
size < fieldSize -> {
119+
buf.put(value)
120+
buf.put(ByteArray(fieldSize - size) { 0 })
121+
}
122+
else -> buf.put(value)
123+
}
124+
}
125+
}
126+
'S' -> buf.put(value as ByteArray)
127+
else -> throw UnsupportedOperationException("Format specifier $field not supported")
128+
}
129+
}
130+
131+
return buf.array()
132+
}
133+
134+
fun unpack(formatString: String, data: ByteArray): List<Any> {
135+
val endianness = formatString.getEndianness()
136+
val format = formatString.getFormatList()
137+
val values = mutableListOf<Any>()
138+
139+
val buf = ByteBuffer.wrap(data).order(endianness)
140+
for ((index, field) in format.withIndex()) {
141+
val spec = field.last()
142+
print("Unpacking field $index - $field($spec) - ")
143+
@Suppress("UsePropertyAccessSyntax")
144+
val value = when (spec) {
145+
'x' -> buf.get()
146+
'c' -> buf.getChar()
147+
'b' -> buf.get().toInt()
148+
'B' -> buf.get().toUByte().toInt()
149+
'?' -> buf.get().toInt() == 1
150+
'h' -> buf.getShort().toInt()
151+
'H' -> buf.getShort().toUShort().toInt()
152+
'i' -> buf.getInt()
153+
'I' -> buf.getInt().toUInt().toInt()
154+
'l' -> buf.getInt().toLong()
155+
'L' -> buf.getInt().toUInt().toLong()
156+
'q' -> buf.getLong()
157+
'Q' -> buf.getLong().toULong().toLong()
158+
'f' -> buf.getFloat()
159+
'd' -> buf.getDouble()
160+
's' -> {
161+
val size = if (field[0] == '$') {
162+
val fieldIndex = field.drop(1).dropLast(1).toInt()
163+
(values[fieldIndex] as Number).toInt()
164+
} else {
165+
field.dropLast(1).toInt()
166+
}
167+
val byteArray = ByteArray(size)
168+
buf.get(byteArray)
169+
byteArray
170+
}
171+
'S' -> data.sliceArray(buf.position() until data.size)
172+
else -> throw UnsupportedOperationException("Format specifier $field not supported")
173+
}
174+
println(value)
175+
if (spec == 'x')
176+
continue
177+
values.add(value)
178+
}
179+
180+
return values
181+
}
182+
}

0 commit comments

Comments
 (0)