Skip to content

Commit ac84222

Browse files
authored
Merge pull request #77 from ngjunsiang/master
fix: Create bitfield structs with the correct size
2 parents 8d1dd7e + 1451748 commit ac84222

1 file changed

Lines changed: 80 additions & 29 deletions

File tree

opendis/record.py

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,85 @@
44
"""
55

66
from collections.abc import Sequence
7-
from ctypes import _SimpleCData, BigEndianStructure, c_uint
7+
from ctypes import (
8+
_SimpleCData,
9+
BigEndianStructure,
10+
c_uint8,
11+
c_uint16,
12+
c_uint32,
13+
sizeof,
14+
)
15+
from typing import Literal
16+
17+
from .stream import DataInputStream, DataOutputStream
818
from .types import (
919
bf_enum,
1020
bf_int,
1121
bf_uint,
1222
)
1323

14-
from .DataInputStream import DataInputStream
15-
from .DataOutputStream import DataOutputStream
16-
17-
18-
def _bitfield(
19-
name: str,
20-
bytesize: int,
21-
fields: Sequence[
22-
tuple[str, type[_SimpleCData]] | tuple[str, type[_SimpleCData], int]
23-
],
24-
):
24+
# Type definitions for bitfield field descriptors
25+
CTypeFieldDescription = tuple[str, type[_SimpleCData], int]
26+
DisFieldDescription = tuple[str, "DisFieldType", int]
27+
28+
# Field type constants simplify the construction of bitfields
29+
# which would otherwise require manually specifying ctypes types.
30+
# The currently implemented bitfields only use integers, but DIS7
31+
# mentions CHAR types which may be needed in future.
32+
DisFieldType = Literal["INTEGER"]
33+
INTEGER = "INTEGER"
34+
35+
36+
def field(name: str,
37+
ftype: DisFieldType,
38+
bits: int) -> CTypeFieldDescription:
39+
"""Helper function to create the field description tuple used by ctypes."""
40+
match (ftype, bits):
41+
case (INTEGER, b) if 0 < b <= 8:
42+
return (name, c_uint8, bits)
43+
case (INTEGER, b) if 8 < b <= 16:
44+
return (name, c_uint16, bits)
45+
case (INTEGER, b) if 16 < b <= 32:
46+
return (name, c_uint32, bits)
47+
case _:
48+
raise ValueError(f"Unrecognized (ftype, bits): {ftype}, {bits}")
49+
50+
51+
def _bitfield(name: str,
52+
fields: Sequence[DisFieldDescription]):
2553
"""Factory function for bitfield structs, which are subclasses of
2654
ctypes.Structure.
2755
These are used in records that require them to unpack non-octet-sized fields.
2856
2957
Args:
3058
name: Name of the bitfield struct.
3159
bytesize: Size of the bitfield in bytes.
32-
fields: Sequence of tuples defining the fields of the bitfield.
33-
See https://docs.python.org/3/library/ctypes.html#ctypes.Structure._fields_
60+
fields: Sequence of tuples defining fields of the bitfield, in the form
61+
(field_name, "INTEGER", field_size_in_bits).
3462
"""
35-
if bytesize <= 0:
36-
raise ValueError("Cannot create bitfield with less than one byte")
37-
63+
# Argument validation
64+
struct_fields = []
65+
bitsize = 0
66+
for name, ftype, bits in fields:
67+
if ftype not in (INTEGER,):
68+
raise ValueError(f"Unsupported field type: {ftype}")
69+
if not isinstance(bits, int):
70+
raise ValueError(f"Field size must be int: {bits!r}")
71+
if bits <= 0 or bits > 32:
72+
raise ValueError(f"Field size must be between 1 and 32: got {bits}")
73+
bitsize += bits
74+
struct_fields.append(field(name, ftype, bits))
75+
76+
if bitsize == 0:
77+
raise ValueError(f"Bitfield size cannot be zero")
78+
elif bitsize % 8 != 0:
79+
raise ValueError(f"Bitfield size must be multiple of 8, got {bitsize}")
80+
bytesize = bitsize // 8
81+
82+
# Create the struct class
3883
class Bitfield(BigEndianStructure):
39-
_fields_ = fields
40-
84+
_fields_ = struct_fields
85+
4186
@staticmethod
4287
def marshalledSize() -> int:
4388
return bytesize
@@ -48,6 +93,12 @@ def serialize(self, outputStream: DataOutputStream) -> None:
4893
@classmethod
4994
def parse(cls, inputStream: DataInputStream) -> "Bitfield":
5095
return cls.from_buffer_copy(inputStream.read_bytes(bytesize))
96+
97+
# Sanity check: ensure the struct size matches expected size
98+
assert sizeof(Bitfield) == bytesize, \
99+
f"Bitfield size mismatch: expected {bytesize}, got {sizeof(Bitfield)}"
100+
101+
# Assign the class name
51102
Bitfield.__name__ = name
52103
return Bitfield
53104

@@ -61,11 +112,11 @@ class NetId:
61112
YY = Frequency Table
62113
"""
63114

64-
_struct = _bitfield(name="NetId", bytesize=2, fields=[
65-
("netNumber", c_uint, 10),
66-
("frequencyTable", c_uint, 2),
67-
("mode", c_uint, 2),
68-
("padding", c_uint, 2)
115+
_struct = _bitfield(name="NetId", fields=[
116+
("netNumber", INTEGER, 10),
117+
("frequencyTable", INTEGER, 2),
118+
("mode", INTEGER, 2),
119+
("padding", INTEGER, 2)
69120
])
70121

71122
def __init__(self,
@@ -113,11 +164,11 @@ class SpreadSpectrum:
113164
In Python, the presence or absence of each technique is indicated by a bool.
114165
"""
115166

116-
_struct = _bitfield("SpreadSpectrum", 2, [
117-
("frequencyHopping", c_uint, 1),
118-
("pseudoNoise", c_uint, 1),
119-
("timeHopping", c_uint, 1),
120-
("padding", c_uint, 13)
167+
_struct = _bitfield(name="SpreadSpectrum", fields=[
168+
("frequencyHopping", INTEGER, 1),
169+
("pseudoNoise", INTEGER, 1),
170+
("timeHopping", INTEGER, 1),
171+
("padding", INTEGER, 13)
121172
])
122173

123174
def __init__(self,

0 commit comments

Comments
 (0)