From d147308e54d3615faddd818d9eb900eedb328d1c Mon Sep 17 00:00:00 2001 From: JS Date: Wed, 1 Oct 2025 00:27:10 +0800 Subject: [PATCH 1/6] feat: add field() helper function This simplifies definition of bitfields, which would otherwise require specifying ctypes ctypes. --- opendis/record.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/opendis/record.py b/opendis/record.py index 7c8b082..a32ad48 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -4,7 +4,15 @@ """ from collections.abc import Sequence -from ctypes import _SimpleCData, BigEndianStructure, c_uint +from ctypes import ( + _SimpleCData, + BigEndianStructure, + c_uint8, + c_uint16, + c_uint32, +) +from typing import Literal + from .types import ( bf_enum, bf_int, @@ -14,6 +22,32 @@ from .DataInputStream import DataInputStream from .DataOutputStream import DataOutputStream +# Type definitions for bitfield field descriptors +CTypeFieldDescription = tuple[str, type[_SimpleCData], int] +DisFieldDescription = tuple[str, "DisFieldType", int] + +# Field type constants simplify the construction of bitfields +# which would otherwise require manually specifying ctypes types. +# The currently implemented bitfields only use integers, but DIS7 +# mentions CHAR types which may be needed in future. +DisFieldType = Literal["INTEGER"] +INTEGER = "INTEGER" + + +def field(name: str, + ftype: DisFieldType, + bits: int) -> CTypeFieldDescription: + """Helper function to create the field description tuple used by ctypes.""" + match (ftype, bits): + case (INTEGER, b) if 0 < b <= 8: + return (name, c_uint8, bits) + case (INTEGER, b) if 8 < b <= 16: + return (name, c_uint16, bits) + case (INTEGER, b) if 16 < b <= 32: + return (name, c_uint32, bits) + case _: + raise ValueError(f"Unrecognized (ftype, bits): {ftype}, {bits}") + def _bitfield( name: str, From efe2e197f950c05724936eb6af93ff31739e5b44 Mon Sep 17 00:00:00 2001 From: JS Date: Wed, 1 Oct 2025 00:28:36 +0800 Subject: [PATCH 2/6] refactor: simplify stream imports --- opendis/record.py | 1 + 1 file changed, 1 insertion(+) diff --git a/opendis/record.py b/opendis/record.py index a32ad48..1b177d0 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -13,6 +13,7 @@ ) from typing import Literal +from .stream import DataInputStream, DataOutputStream from .types import ( bf_enum, bf_int, From d1bec6ebaed84016821cc94ffc5f7e1945c16d3f Mon Sep 17 00:00:00 2001 From: JS Date: Wed, 1 Oct 2025 00:36:19 +0800 Subject: [PATCH 3/6] fix: construct bitfields with the correct struct size The buggy code used c_uint for all integer fields, resulting in the SpreadSpectrum struct being padded out to 32 bits instead of being 16 bits. The fix uses the field() helper function to specify the correct ctype to use, greatly simplifying the process of defining bitfields. --- opendis/record.py | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/opendis/record.py b/opendis/record.py index 1b177d0..0a8688a 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -20,9 +20,6 @@ bf_uint, ) -from .DataInputStream import DataInputStream -from .DataOutputStream import DataOutputStream - # Type definitions for bitfield field descriptors CTypeFieldDescription = tuple[str, type[_SimpleCData], int] DisFieldDescription = tuple[str, "DisFieldType", int] @@ -53,9 +50,7 @@ def field(name: str, def _bitfield( name: str, bytesize: int, - fields: Sequence[ - tuple[str, type[_SimpleCData]] | tuple[str, type[_SimpleCData], int] - ], + fields: Sequence[DisFieldDescription], ): """Factory function for bitfield structs, which are subclasses of ctypes.Structure. @@ -64,18 +59,23 @@ def _bitfield( Args: name: Name of the bitfield struct. bytesize: Size of the bitfield in bytes. - fields: Sequence of tuples defining the fields of the bitfield. - See https://docs.python.org/3/library/ctypes.html#ctypes.Structure._fields_ + fields: Sequence of tuples defining fields of the bitfield, in the form + (field_name, "INTEGER", field_size_in_bits). """ - if bytesize <= 0: - raise ValueError("Cannot create bitfield with less than one byte") - + # Argument validation + struct_fields = [] + bitsize = 0 + for name, ftype, bits in fields: + bitsize += bits + struct_fields.append(field(name, ftype, bits)) + + # Create the struct class class Bitfield(BigEndianStructure): - _fields_ = fields - + _fields_ = struct_fields + @staticmethod def marshalledSize() -> int: - return bytesize + return bitsize // 8 def serialize(self, outputStream: DataOutputStream) -> None: outputStream.write_bytes(bytes(self)) @@ -83,6 +83,8 @@ def serialize(self, outputStream: DataOutputStream) -> None: @classmethod def parse(cls, inputStream: DataInputStream) -> "Bitfield": return cls.from_buffer_copy(inputStream.read_bytes(bytesize)) + + # Assign the class name Bitfield.__name__ = name return Bitfield @@ -97,10 +99,10 @@ class NetId: """ _struct = _bitfield(name="NetId", bytesize=2, fields=[ - ("netNumber", c_uint, 10), - ("frequencyTable", c_uint, 2), - ("mode", c_uint, 2), - ("padding", c_uint, 2) + ("netNumber", INTEGER, 10), + ("frequencyTable", INTEGER, 2), + ("mode", INTEGER, 2), + ("padding", INTEGER, 2) ]) def __init__(self, @@ -149,10 +151,10 @@ class SpreadSpectrum: """ _struct = _bitfield("SpreadSpectrum", 2, [ - ("frequencyHopping", c_uint, 1), - ("pseudoNoise", c_uint, 1), - ("timeHopping", c_uint, 1), - ("padding", c_uint, 13) + ("frequencyHopping", INTEGER, 1), + ("pseudoNoise", INTEGER, 1), + ("timeHopping", INTEGER, 1), + ("padding", INTEGER, 13) ]) def __init__(self, From 9776c3346d5cf8dd0f10181977016edd3173cf8d Mon Sep 17 00:00:00 2001 From: JS Date: Wed, 1 Oct 2025 00:38:58 +0800 Subject: [PATCH 4/6] feat: validate bitfield factory arguments --- opendis/record.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/opendis/record.py b/opendis/record.py index 0a8688a..978b256 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -66,16 +66,28 @@ def _bitfield( struct_fields = [] bitsize = 0 for name, ftype, bits in fields: + if ftype not in (INTEGER,): + raise ValueError(f"Unsupported field type: {ftype}") + if not isinstance(bits, int): + raise ValueError(f"Field size must be int: {bits!r}") + if bits <= 0 or bits > 32: + raise ValueError(f"Field size must be between 1 and 32: got {bits}") bitsize += bits struct_fields.append(field(name, ftype, bits)) + if bitsize == 0: + raise ValueError(f"Bitfield size cannot be zero") + elif bitsize % 8 != 0: + raise ValueError(f"Bitfield size must be multiple of 8, got {bitsize}") + bytesize = bitsize // 8 + # Create the struct class class Bitfield(BigEndianStructure): _fields_ = struct_fields @staticmethod def marshalledSize() -> int: - return bitsize // 8 + return bytesize def serialize(self, outputStream: DataOutputStream) -> None: outputStream.write_bytes(bytes(self)) From 399852f95c3e593f3e511cc82032c76926a7a0a4 Mon Sep 17 00:00:00 2001 From: JS Date: Wed, 1 Oct 2025 00:39:40 +0800 Subject: [PATCH 5/6] feat: verify bitfield struct size --- opendis/record.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/opendis/record.py b/opendis/record.py index 978b256..7c482bc 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -10,6 +10,7 @@ c_uint8, c_uint16, c_uint32, + sizeof, ) from typing import Literal @@ -96,6 +97,10 @@ def serialize(self, outputStream: DataOutputStream) -> None: def parse(cls, inputStream: DataInputStream) -> "Bitfield": return cls.from_buffer_copy(inputStream.read_bytes(bytesize)) + # Sanity check: ensure the struct size matches expected size + assert sizeof(Bitfield) == bytesize, \ + f"Bitfield size mismatch: expected {bytesize}, got {sizeof(Bitfield)}" + # Assign the class name Bitfield.__name__ = name return Bitfield From 14517482ba763712d80667472f357360c740a1f5 Mon Sep 17 00:00:00 2001 From: JS Date: Wed, 1 Oct 2025 00:41:27 +0800 Subject: [PATCH 6/6] refactor: remove bitfield factory bytesize parameter The bytesize can be calculated from the field descriptions, simplifying the interface and reducing the chance of human error. --- opendis/record.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/opendis/record.py b/opendis/record.py index 7c482bc..211cd8a 100644 --- a/opendis/record.py +++ b/opendis/record.py @@ -48,11 +48,8 @@ def field(name: str, raise ValueError(f"Unrecognized (ftype, bits): {ftype}, {bits}") -def _bitfield( - name: str, - bytesize: int, - fields: Sequence[DisFieldDescription], - ): +def _bitfield(name: str, + fields: Sequence[DisFieldDescription]): """Factory function for bitfield structs, which are subclasses of ctypes.Structure. These are used in records that require them to unpack non-octet-sized fields. @@ -115,7 +112,7 @@ class NetId: YY = Frequency Table """ - _struct = _bitfield(name="NetId", bytesize=2, fields=[ + _struct = _bitfield(name="NetId", fields=[ ("netNumber", INTEGER, 10), ("frequencyTable", INTEGER, 2), ("mode", INTEGER, 2), @@ -167,7 +164,7 @@ class SpreadSpectrum: In Python, the presence or absence of each technique is indicated by a bool. """ - _struct = _bitfield("SpreadSpectrum", 2, [ + _struct = _bitfield(name="SpreadSpectrum", fields=[ ("frequencyHopping", INTEGER, 1), ("pseudoNoise", INTEGER, 1), ("timeHopping", INTEGER, 1),