@@ -359,6 +359,126 @@ class PackedFormatTest {
359359 }
360360 }
361361
362+ // --- Size assertions ---
363+
364+ @Test
365+ fun `class with no booleans or nullables has no bitmask header` () {
366+ // 4 (Int) + 8 (Long) + 1 (VarInt len) + 13 (String bytes) = 26
367+ val bytes = PackedFormat .encodeToByteArray(
368+ NoBooleansNoNulls .serializer(),
369+ NoBooleansNoNulls (42 , 42L , " No flags here" )
370+ )
371+ assertEquals(26 , bytes.size)
372+ }
373+
374+ @Test
375+ fun `boolean fields pack into single bitmask byte` () {
376+ // 2 booleans → 1 bitmask byte (VarLong), 2 ints → 4 bytes each = 9 total
377+ val bytes = PackedFormat .encodeToByteArray(
378+ SimpleIntsAndBooleans .serializer(),
379+ SimpleIntsAndBooleans (0 , 0 , false , false )
380+ )
381+ assertEquals(9 , bytes.size)
382+ }
383+
384+ @Test
385+ fun `empty string encodes as single zero byte` () {
386+ val bytes = PackedFormat .encodeToByteArray(String .serializer(), " " )
387+ assertEquals(1 , bytes.size)
388+ assertEquals(0 , bytes[0 ])
389+ }
390+
391+ @Test
392+ fun `empty list encodes as single zero byte` () {
393+ val bytes = PackedFormat .encodeToByteArray(ListSerializer (Int .serializer()), emptyList())
394+ assertEquals(1 , bytes.size)
395+ assertEquals(0 , bytes[0 ])
396+ }
397+
398+ @Test
399+ fun `empty map roundtrip` () {
400+ assertPackedRoundtrip(MapHolder .serializer(), MapHolder (emptyMap(), null ))
401+ assertPackedRoundtrip(MapHolder .serializer(), MapHolder (emptyMap(), emptyMap()))
402+ }
403+
404+ // --- VarInt / VarUInt encoding distinction ---
405+
406+ @Test
407+ fun `VarUInt does not apply zigzag so negative inputs encode large` () {
408+ // @VarUInt(-1 as Int) = 0xFFFFFFFF unsigned = 5 bytes
409+ // @VarUInt(-1L as Long) = 0xFFFFFFFFFFFFFFFF unsigned = 10 bytes
410+ val uintNegative = VarIntVarUIntPayload (0 , 0L , - 1 , - 1L , 0 , 0 )
411+ val bytes = PackedFormat .encodeToByteArray(VarIntVarUIntPayload .serializer(), uintNegative)
412+ // VarInt(0)=1, VarLong(0)=1, VarInt(0xFFFFFFFF)=5, VarLong(0xFFFFFFFF…)=10, Int=4, Long=8 → 29
413+ assertEquals(29 , bytes.size)
414+
415+ // @VarInt(-1) = zigzag(−1) = 1 = 1 byte
416+ val intNegative = VarIntVarUIntPayload (- 1 , - 1L , 0 , 0L , 0 , 0 )
417+ val bytes2 = PackedFormat .encodeToByteArray(VarIntVarUIntPayload .serializer(), intNegative)
418+ // VarInt(1)=1, VarLong(1)=1, VarInt(0)=1, VarLong(0)=1, Int=4, Long=8 → 16
419+ assertEquals(16 , bytes2.size)
420+
421+ assertPackedRoundtrip(VarIntVarUIntPayload .serializer(), uintNegative)
422+ assertPackedRoundtrip(VarIntVarUIntPayload .serializer(), intNegative)
423+ }
424+
425+ // --- defaultVarInt / defaultZigZag interaction ---
426+
427+ @Test
428+ fun `both defaultVarInt and defaultZigZag active uses zigzag` () {
429+ val fmt = PackedFormat { defaultVarInt = true ; defaultZigZag = true }
430+ val payload = UnannotatedPayload (- 1 , - 1L )
431+ val bytes = fmt.encodeToByteArray(UnannotatedPayload .serializer(), payload)
432+ // zigzag(−1)=1 for both → 1 + 1 = 2 bytes
433+ assertEquals(2 , bytes.size)
434+ assertEquals(payload, fmt.decodeFromByteArray(UnannotatedPayload .serializer(), bytes))
435+ }
436+
437+ // --- Nullability roundtrips not covered in the main table ---
438+
439+ @Test
440+ fun `deep null mid-chain roundtrip` () {
441+ assertPackedRoundtrip(DeepNested .serializer(), DeepNested (" root" , Level1 (false , null )))
442+ }
443+
444+ // --- UTF-8 character encoding ---
445+
446+ @Test
447+ fun `3-byte UTF-8 char roundtrip` () {
448+ assertPackedRoundtrip(
449+ AllPrimitiveTypes .serializer(),
450+ AllPrimitiveTypes (0 , 0L , 0 , 0 , 0f , 0.0 , ' 中' , false , " " )
451+ )
452+ }
453+
454+ // --- Truncation error coverage ---
455+
456+ @Test
457+ fun `truncated varint throws` () {
458+ // Continuation bit set but no following byte
459+ val incomplete = byteArrayOf(0x80 .toByte())
460+ assertFailsWith<IllegalArgumentException > {
461+ PackedUtils .decodeVarInt(incomplete, 0 )
462+ }
463+ }
464+
465+ @Test
466+ fun `truncated string bytes throws` () {
467+ // VarInt says length 5 but only 2 bytes follow
468+ val bad = byteArrayOf(5 , ' h' .code.toByte(), ' i' .code.toByte())
469+ assertFailsWith<IllegalArgumentException > {
470+ PackedFormat .decodeFromByteArray(String .serializer(), bad)
471+ }
472+ }
473+
474+ @Test
475+ fun `truncated bitmask header throws` () {
476+ // NullableFieldsPayload expects a bitmask but gets empty input
477+ assertFailsWith<IllegalArgumentException > {
478+ PackedFormat .decodeFromByteArray(NullableFieldsPayload .serializer(), byteArrayOf())
479+ }
480+ }
481+
362482 @Test
363483 fun `decodeElementIndex simulates sequential decoding for classes and collections` () {
364484 // 1. Test the Class path
0 commit comments