Skip to content

Commit 0322b04

Browse files
committed
added checks for invalid inputs
1 parent 72a92b0 commit 0322b04

3 files changed

Lines changed: 142 additions & 18 deletions

File tree

src/main/kotlin/com/eignex/kencode/PackedDecoder.kt

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class PackedDecoder(
6767
return if (inStructure) {
6868
decodeBooleanElement(currentDescriptor, currentIndex)
6969
} else {
70+
require(position < input.size) {
71+
"Unexpected EOF while decoding Boolean"
72+
}
7073
input[position++].toInt() != 0
7174
}
7275
}
@@ -75,6 +78,9 @@ class PackedDecoder(
7578
return if (inStructure) {
7679
decodeByteElement(currentDescriptor, currentIndex)
7780
} else {
81+
require(position < input.size) {
82+
"Unexpected EOF while decoding Byte"
83+
}
7884
input[position++]
7985
}
8086
}
@@ -133,6 +139,9 @@ class PackedDecoder(
133139
} else {
134140
val (len, bytesRead) = PackedUtils.decodeVarInt(input, position)
135141
position += bytesRead
142+
require(len >= 0 && position + len <= input.size) {
143+
"Unexpected EOF while decoding String payload: need $len bytes from position=$position, size=${input.size}"
144+
}
136145
val bytes = input.copyOfRange(position, position + len)
137146
position += len
138147
bytes.toString(Charsets.UTF_8)
@@ -248,6 +257,9 @@ class PackedDecoder(
248257
descriptor: SerialDescriptor,
249258
index: Int
250259
): Byte {
260+
require(position < input.size) {
261+
"Unexpected EOF while decoding Byte element at index $index"
262+
}
251263
return input[position++]
252264
}
253265

@@ -285,6 +297,9 @@ class PackedDecoder(
285297
): String {
286298
val (len, bytesRead) = PackedUtils.decodeVarInt(input, position)
287299
position += bytesRead
300+
require(len >= 0 && position + len <= input.size) {
301+
"Unexpected EOF while decoding String element at index $index: need $len bytes from position=$position, size=${input.size}"
302+
}
288303
val bytes = input.copyOfRange(position, position + len)
289304
position += len
290305
return bytes.toString(Charsets.UTF_8)
@@ -369,10 +384,9 @@ class PackedDecoder(
369384
}
370385
}
371386

372-
373387
private fun readUtf8Char(): Char {
374-
if (position >= input.size) {
375-
error("Unexpected EOF while decoding UTF-8 char")
388+
require(position < input.size) {
389+
"Unexpected EOF while decoding UTF-8 char"
376390
}
377391

378392
val b0 = input[position].toInt() and 0xFF
@@ -385,10 +399,12 @@ class PackedDecoder(
385399

386400
// 2-byte: 110xxxxx 10xxxxxx
387401
(b0 and 0b1110_0000) == 0b1100_0000 -> {
388-
if (position + 2 > input.size) error("Unexpected EOF in 2-byte UTF-8 char")
402+
require(position + 2 <= input.size) {
403+
"Unexpected EOF in 2-byte UTF-8 char"
404+
}
389405
val b1 = input[position + 1].toInt() and 0xFF
390-
if ((b1 and 0b1100_0000) != 0b1000_0000) {
391-
error("Invalid UTF-8 continuation byte: 0x${b1.toString(16)}")
406+
require((b1 and 0b1100_0000) == 0b1000_0000) {
407+
"Invalid UTF-8 continuation byte: 0x${b1.toString(16)}"
392408
}
393409
val codePoint =
394410
((b0 and 0b0001_1111) shl 6) or
@@ -398,13 +414,14 @@ class PackedDecoder(
398414

399415
// 3-byte: 1110xxxx 10xxxxxx 10xxxxxx
400416
(b0 and 0b1111_0000) == 0b1110_0000 -> {
401-
if (position + 3 > input.size) error("Unexpected EOF in 3-byte UTF-8 char")
417+
require(position + 3 <= input.size) {
418+
"Unexpected EOF in 3-byte UTF-8 char"
419+
}
402420
val b1 = input[position + 1].toInt() and 0xFF
403421
val b2 = input[position + 2].toInt() and 0xFF
404-
if ((b1 and 0b1100_0000) != 0b1000_0000 ||
405-
(b2 and 0b1100_0000) != 0b1000_0000
406-
) {
407-
error("Invalid UTF-8 continuation byte in 3-byte char")
422+
require((b1 and 0b1100_0000) == 0b1000_0000 &&
423+
(b2 and 0b1100_0000) == 0b1000_0000) {
424+
"Invalid UTF-8 continuation byte in 3-byte char"
408425
}
409426
val codePoint =
410427
((b0 and 0b0000_1111) shl 12) or
@@ -413,7 +430,9 @@ class PackedDecoder(
413430
3 to codePoint
414431
}
415432

416-
else -> error("UTF-8 sequence too long for Char (leading byte: 0x${b0.toString(16)})")
433+
else -> throw IllegalArgumentException(
434+
"UTF-8 sequence too long for Char (leading byte: 0x${b0.toString(16)})"
435+
)
417436
}
418437

419438
position += len
@@ -422,5 +441,4 @@ class PackedDecoder(
422441
// since Char is a UTF-16 code unit.
423442
return cp.toChar()
424443
}
425-
426444
}

src/main/kotlin/com/eignex/kencode/PackedUtils.kt

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@ package com.eignex.kencode
33
import java.io.ByteArrayOutputStream
44

55
internal object PackedUtils {
6-
// flags -> int/long
6+
7+
private fun requireAvailable(
8+
data: ByteArray,
9+
offset: Int,
10+
needed: Int,
11+
what: String
12+
) {
13+
require(offset >= 0 && needed >= 0 && offset + needed <= data.size) {
14+
"Unexpected EOF while decoding $what: need $needed bytes from offset=$offset, size=${data.size}"
15+
}
16+
}
17+
718
fun packFlagsToLong(vararg flags: Boolean): Long {
819
var result = 0L
920
for (i in flags.indices) {
@@ -57,12 +68,14 @@ internal object PackedUtils {
5768
}
5869

5970
fun readShort(data: ByteArray, offset: Int): Short {
71+
requireAvailable(data, offset, 2, "Short")
6072
val b0 = data[offset].toInt() and 0xFF
6173
val b1 = data[offset + 1].toInt() and 0xFF
6274
return ((b0 shl 8) or b1).toShort()
6375
}
6476

6577
fun readInt(data: ByteArray, offset: Int): Int {
78+
requireAvailable(data, offset, 4, "Int")
6679
var v = 0
6780
for (i in 0 until 4) {
6881
v = (v shl 8) or (data[offset + i].toInt() and 0xFF)
@@ -71,13 +84,17 @@ internal object PackedUtils {
7184
}
7285

7386
fun readLong(data: ByteArray, offset: Int): Long {
87+
requireAvailable(data, offset, 8, "Long")
7488
var v = 0L
7589
for (i in 0 until 8) {
7690
v = (v shl 8) or (data[offset + i].toLong() and 0xFFL)
7791
}
7892
return v
7993
}
8094

95+
// ------------------------------------------------------------
96+
// LEB128 VarInt / VarLong writers
97+
// ------------------------------------------------------------
8198
fun writeVarInt(value: Int, out: ByteArrayOutputStream) {
8299
var v = value
83100
while (true) {
@@ -104,19 +121,24 @@ internal object PackedUtils {
104121
}
105122
}
106123

124+
// ------------------------------------------------------------
125+
// LEB128 VarInt / VarLong decoders (with EOF checking)
126+
// ------------------------------------------------------------
107127
fun decodeVarInt(input: ByteArray, offset: Int): Pair<Int, Int> {
108128
var result = 0
109129
var shift = 0
110130
var pos = offset
111131
while (true) {
112-
if (pos >= input.size) error("Unexpected EOF while decoding VarInt")
132+
require(pos < input.size) {
133+
"Unexpected EOF while decoding VarInt"
134+
}
113135
val b = input[pos++].toInt() and 0xFF
114136
result = result or ((b and 0x7F) shl shift)
115137
if (b and 0x80 == 0) {
116138
return result to (pos - offset)
117139
}
118140
shift += 7
119-
if (shift > 35) error("VarInt too long")
141+
require(shift <= 35) { "VarInt too long" }
120142
}
121143
}
122144

@@ -125,14 +147,16 @@ internal object PackedUtils {
125147
var shift = 0
126148
var pos = offset
127149
while (true) {
128-
if (pos >= input.size) error("Unexpected EOF while decoding VarLong")
150+
require(pos < input.size) {
151+
"Unexpected EOF while decoding VarLong"
152+
}
129153
val b = input[pos++].toInt() and 0xFF
130154
result = result or ((b and 0x7F).toLong() shl shift)
131155
if (b and 0x80 == 0) {
132156
return result to (pos - offset)
133157
}
134158
shift += 7
135-
if (shift > 70) error("VarLong too long")
159+
require(shift <= 70) { "VarLong too long" }
136160
}
137161
}
138162
}

src/test/kotlin/com/eignex/kencode/PackedFormatTest.kt

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ class PackedFormatTest {
115115
val seq: Int
116116
)
117117

118+
@Serializable
119+
data class Child(val value: Int)
120+
121+
@Serializable
122+
data class Parent(val id: Int, val child: Child)
123+
124+
@Serializable
125+
data class WithList(val id: Int, val items: List<Int>)
126+
127+
118128
private fun <T> roundtrip(
119129
serializer: KSerializer<T>, value: T
120130
) {
@@ -518,4 +528,76 @@ class PackedFormatTest {
518528
assertNull(result)
519529
}
520530

531+
@Test
532+
fun `single continuation byte should throw for top level char`() {
533+
// 0x80 is a continuation byte without a valid leading byte.
534+
val bytes = byteArrayOf(0x80.toByte())
535+
val decoder = PackedDecoder(bytes)
536+
537+
assertFailsWith<IllegalArgumentException> {
538+
decoder.decodeSerializableValue(Char.serializer())
539+
}
540+
}
541+
542+
@Test
543+
fun `truncated 2-byte sequence should throw for top level char`() {
544+
// 0xC2 is a valid leading byte for a 2-byte sequence, but we don't
545+
// provide the required continuation byte → Unexpected EOF.
546+
val bytes = byteArrayOf(0xC2.toByte())
547+
val decoder = PackedDecoder(bytes)
548+
549+
assertFailsWith<IllegalArgumentException> {
550+
decoder.decodeSerializableValue(Char.serializer())
551+
}
552+
}
553+
554+
@Test
555+
fun `invalid continuation byte should throw for top level char`() {
556+
// 0xC2 expects a continuation byte (10xxxxxx); 0x41 ('A') is invalid.
557+
val bytes = byteArrayOf(0xC2.toByte(), 0x41)
558+
val decoder = PackedDecoder(bytes)
559+
560+
assertFailsWith<IllegalArgumentException> {
561+
decoder.decodeSerializableValue(Char.serializer())
562+
}
563+
}
564+
565+
@Test
566+
fun `encoding nested class with PackedFormat should throw`() {
567+
val value = Parent(id = 1, child = Child(2))
568+
569+
assertFailsWith<IllegalStateException> {
570+
PackedFormat.encodeToByteArray(Parent.serializer(), value)
571+
}
572+
}
573+
574+
@Test
575+
fun `encoding list field with PackedFormat should throw`() {
576+
val value = WithList(id = 1, items = listOf(1, 2, 3))
577+
578+
assertFailsWith<IllegalStateException> {
579+
PackedFormat.encodeToByteArray(WithList.serializer(), value)
580+
}
581+
}
582+
583+
@Test
584+
fun `decoding nested class with PackedFormat should throw`() {
585+
// Minimal bytes: a single 0x00 varlong for flags; actual payload is irrelevant
586+
// because the decoder rejects nested structures based on the descriptor
587+
// before deserializing the nested value.
588+
val bytes = byteArrayOf(0x00)
589+
590+
assertFailsWith<IllegalArgumentException> {
591+
PackedFormat.decodeFromByteArray(Parent.serializer(), bytes)
592+
}
593+
}
594+
595+
@Test
596+
fun `decoding list field with PackedFormat should throw`() {
597+
val bytes = byteArrayOf(0x00)
598+
599+
assertFailsWith<IllegalArgumentException> {
600+
PackedFormat.decodeFromByteArray(WithList.serializer(), bytes)
601+
}
602+
}
521603
}

0 commit comments

Comments
 (0)