44"""
55
66from 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
818from .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