|
9 | 9 |
|
10 | 10 | from typing import Annotated |
11 | 11 |
|
12 | | -from libdestruct import array, c_int, c_long, c_short, c_uint, inflater, offset, struct, ptr, ptr_to_self, array_of, enum, enum_of |
| 12 | +from libdestruct import array, c_int, c_long, c_short, c_uint, c_ushort, inflater, offset, struct, ptr, ptr_to_self, array_of, enum, enum_of, size_of, bitfield_of |
| 13 | +from libdestruct.common.union import union, union_of, tagged_union |
13 | 14 |
|
14 | 15 |
|
15 | 16 | class StructMemberCollisionTest(unittest.TestCase): |
@@ -419,5 +420,109 @@ class s_t(struct): |
419 | 420 | self.assertIs(s.__eq__(42), NotImplemented) |
420 | 421 |
|
421 | 422 |
|
| 423 | +class BitfieldExplicitOffsetTest(unittest.TestCase): |
| 424 | + """Explicit offset after bitfields must flush the pending bitfield group first.""" |
| 425 | + |
| 426 | + def test_offset_after_bitfield_size(self): |
| 427 | + """Struct size must be correct when offset() follows bitfield fields.""" |
| 428 | + class s_t(struct): |
| 429 | + a: c_uint = bitfield_of(c_uint, 1) |
| 430 | + b: c_int = offset(8) |
| 431 | + |
| 432 | + # a is a 1-bit bitfield in a 4-byte c_uint group at offset 0. |
| 433 | + # b is at explicit offset 8 with size 4. |
| 434 | + # Total size: 8 + 4 = 12 |
| 435 | + self.assertEqual(size_of(s_t), 12) |
| 436 | + |
| 437 | + def test_offset_after_bitfield_read(self): |
| 438 | + """Values must be read correctly when offset() follows bitfield fields.""" |
| 439 | + import struct as pystruct |
| 440 | + |
| 441 | + class s_t(struct): |
| 442 | + a: c_uint = bitfield_of(c_uint, 1) |
| 443 | + b: c_int = offset(8) |
| 444 | + |
| 445 | + memory = bytearray(12) |
| 446 | + memory[0:4] = pystruct.pack("<I", 1) # a = 1 |
| 447 | + memory[8:12] = pystruct.pack("<i", 42) # b = 42 |
| 448 | + |
| 449 | + s = s_t.from_bytes(memory) |
| 450 | + self.assertEqual(s.a.value, 1) |
| 451 | + self.assertEqual(s.b.value, 42) |
| 452 | + |
| 453 | + |
| 454 | +class UnionAlignmentTest(unittest.TestCase): |
| 455 | + """Union fields in aligned structs must use member-derived alignment.""" |
| 456 | + |
| 457 | + def test_plain_union_alignment_non_power_of_two_size(self): |
| 458 | + """Union of a 12-byte packed struct and c_long: size=12 but alignment must be 8 (from c_long).""" |
| 459 | + class triple_t(struct): |
| 460 | + a: c_int |
| 461 | + b: c_int |
| 462 | + c: c_int |
| 463 | + |
| 464 | + # triple_t is a packed 12-byte struct with alignment 1 |
| 465 | + # c_long is 8 bytes with alignment 8 |
| 466 | + # union size = 12, but alignment should be 8 (max member alignment) |
| 467 | + class s_t(struct): |
| 468 | + _aligned_ = True |
| 469 | + tag: c_short # 2 bytes, align 2 |
| 470 | + data: union = union_of({"t": triple_t, "l": c_long}) |
| 471 | + |
| 472 | + # tag at offset 0 (2 bytes) |
| 473 | + # data alignment = 8 → data at offset 8 |
| 474 | + # data size = 12 |
| 475 | + # struct max alignment = 8 → total = _align_offset(20, 8) = 24 |
| 476 | + self.assertEqual(size_of(s_t), 24) |
| 477 | + |
| 478 | + def test_tagged_union_alignment_non_power_of_two_size(self): |
| 479 | + """Tagged union of a 12-byte packed struct and c_long: alignment must be 8.""" |
| 480 | + class triple_t(struct): |
| 481 | + a: c_int |
| 482 | + b: c_int |
| 483 | + c: c_int |
| 484 | + |
| 485 | + class s_t(struct): |
| 486 | + _aligned_ = True |
| 487 | + tag: c_int # 4 bytes, align 4 |
| 488 | + data: union = tagged_union("tag", {0: triple_t, 1: c_long}) |
| 489 | + |
| 490 | + # tag at offset 0 (4 bytes) |
| 491 | + # data alignment = 8 → data at offset 8 |
| 492 | + # data size = 12 |
| 493 | + # struct max alignment = 8 → total = _align_offset(20, 8) = 24 |
| 494 | + self.assertEqual(size_of(s_t), 24) |
| 495 | + |
| 496 | + |
| 497 | +class SubscriptedEnumUnsignedTest(unittest.TestCase): |
| 498 | + """enum[E, unsigned_backing] must preserve signedness.""" |
| 499 | + |
| 500 | + def test_enum_unsigned_backing(self): |
| 501 | + """enum[E, c_ushort] should correctly decode values exceeding signed range.""" |
| 502 | + from enum import IntEnum |
| 503 | + |
| 504 | + class E(IntEnum): |
| 505 | + MAX_VAL = 0xFFFF |
| 506 | + |
| 507 | + class s_t(struct): |
| 508 | + val: enum[E, c_ushort] |
| 509 | + |
| 510 | + memory = (0xFFFF).to_bytes(2, "little") |
| 511 | + s = s_t.from_bytes(memory) |
| 512 | + self.assertEqual(s.val.value, E.MAX_VAL) |
| 513 | + |
| 514 | + def test_enum_unsigned_size(self): |
| 515 | + """enum[E, c_ushort] struct should be 2 bytes.""" |
| 516 | + from enum import IntEnum |
| 517 | + |
| 518 | + class E(IntEnum): |
| 519 | + A = 0 |
| 520 | + |
| 521 | + class s_t(struct): |
| 522 | + val: enum[E, c_ushort] |
| 523 | + |
| 524 | + self.assertEqual(size_of(s_t), 2) |
| 525 | + |
| 526 | + |
422 | 527 | if __name__ == "__main__": |
423 | 528 | unittest.main() |
0 commit comments