Skip to content

Commit 2789d12

Browse files
committed
flexible array support
1 parent 2768673 commit 2789d12

17 files changed

Lines changed: 1058 additions & 277 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ C-style structs for Python
88
[![PyPI](https://img.shields.io/pypi/pyversions/cstruct.svg)](https://pypi.org/project/cstruct)
99
[![Downloads](https://pepy.tech/badge/cstruct/month)](https://pepy.tech/project/cstruct)
1010
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
11-
11+
[![Known Vulnerabilities](https://snyk.io/test/github/andreax79/python-cstruct/badge.svg)](https://snyk.io/test/github/andreax79/python-cstruct)
1212

1313
Convert C struct/union definitions into Python classes with methods for
1414
serializing/deserializing.

changelog.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,11 @@
127127
### Fix
128128

129129
- Fix compare with None
130+
131+
### 2.4
132+
133+
2022-XX-XX
134+
135+
### Added
136+
137+
- flexible array support

cstruct/__init__.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
__date__ = '15 August 2013'
3131

3232
import struct
33-
from typing import Any, Type
33+
from typing import Any, Dict, Optional, Type
3434
from .base import (
3535
LITTLE_ENDIAN,
3636
BIG_ENDIAN,
@@ -44,6 +44,7 @@
4444
)
4545
from .abstract import CStructMeta, AbstractCStruct
4646
from .cstruct import CStruct
47+
from .c_parser import parse_def
4748
from .mem_cstruct import MemCStruct
4849

4950
__all__ = [
@@ -56,6 +57,7 @@
5657
'MemCStruct',
5758
'define',
5859
'undef',
60+
'getdef',
5961
'typedef',
6062
'sizeof',
6163
'parse',
@@ -81,6 +83,15 @@ def undef(key: str) -> None:
8183
del DEFINES[key]
8284

8385

86+
def getdef(key: str) -> Any:
87+
"""
88+
Return the value for a constant
89+
90+
:param key: identifier
91+
"""
92+
return DEFINES[key]
93+
94+
8495
def typedef(type_: str, alias: str) -> None:
8596
"""
8697
Define an alias name for a data type
@@ -108,7 +119,7 @@ def sizeof(type_: str) -> int:
108119
if t is None:
109120
raise KeyError("Unknow %s \"%s\"" % (kind, type_))
110121
else:
111-
return t.size
122+
return t.sizeof()
112123
else:
113124
ttype = C_TYPE_TO_FORMAT.get(type_, None)
114125
if ttype is None:
@@ -117,9 +128,12 @@ def sizeof(type_: str) -> int:
117128
return struct.calcsize(ttype)
118129

119130

120-
def parse(__struct__: str, __cls__: Type[Any] = None, **kargs: Any) -> AbstractCStruct:
131+
def parse(
132+
__struct__: str, __cls__: Optional[Type[AbstractCStruct]] = None, __name__: Optional[str] = None, **kargs: Dict[str, Any]
133+
) -> Optional[Type[AbstractCStruct]]:
121134
"""
122135
Return a new class mapping a C struct/union definition.
136+
If the string does not contains any definition, return None.
123137
124138
:param __struct__: definition of the struct (or union) in C syntax
125139
:param __cls__: (optional) super class - CStruct(default) or MemCStruct
@@ -130,4 +144,8 @@ def parse(__struct__: str, __cls__: Type[Any] = None, **kargs: Any) -> AbstractC
130144
"""
131145
if __cls__ is None:
132146
__cls__ = CStruct
133-
return __cls__.parse(__struct__, **kargs)
147+
cls_def = parse_def(__struct__, __cls__=__cls__, **kargs)
148+
if cls_def is None:
149+
return None
150+
else:
151+
return __cls__.parse(cls_def, __name__, **kargs)

cstruct/abstract.py

Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,30 +25,35 @@
2525
#
2626

2727
from abc import ABCMeta
28-
from typing import cast, Any, BinaryIO, Optional, Type, Union
28+
from collections import OrderedDict
29+
from typing import Any, BinaryIO, List, Dict, Optional, Type, Tuple, Union
2930
from .base import STRUCTS
3031
import hashlib
3132
from .c_parser import parse_struct, parse_def, Tokens
33+
from .field import calculate_padding, FieldType
3234

3335
__all__ = ['CStructMeta', 'AbstractCStruct']
3436

3537

3638
class CStructMeta(ABCMeta):
37-
def __new__(cls, name: str, bases, dct):
38-
__struct__ = dct.get("__struct__", None)
39-
dct['__cls__'] = bases[0]
39+
__size__: int = 0
40+
41+
# def __new__(cls: Type[type], name: str, bases: tuple, classdict: dict) -> MetaClass:
42+
def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]:
43+
__struct__ = namespace.get('__struct__', None)
44+
namespace['__cls__'] = bases[0] if bases else None
4045
# Parse the struct
41-
if '__struct__' in dct:
42-
if isinstance(dct['__struct__'], ("".__class__, u"".__class__, Tokens)):
43-
dct.update(parse_struct(**dct))
46+
if '__struct__' in namespace:
47+
if isinstance(namespace['__struct__'], (str, Tokens)):
48+
namespace.update(parse_struct(**namespace))
4449
__struct__ = True
45-
if '__def__' in dct:
46-
dct.update(parse_def(**dct))
50+
if '__def__' in namespace:
51+
namespace.update(parse_def(**namespace))
4752
__struct__ = True
4853
# Create the new class
49-
new_class = type.__new__(cls, name, bases, dct)
54+
new_class: Type[Any] = super().__new__(metacls, name, bases, namespace)
5055
# Register the class
51-
if __struct__ is not None and not dct.get('__anonymous__'):
56+
if __struct__ is not None and not namespace.get('__anonymous__'):
5257
STRUCTS[name] = new_class
5358
return new_class
5459

@@ -57,29 +62,38 @@ def __len__(cls) -> int:
5762

5863
@property
5964
def size(cls) -> int:
60-
"""Structure size (in bytes)"""
65+
"Structure size (in bytes)"
6166
return cls.__size__
6267

6368

64-
# Workaround for Python 2.x/3.x metaclass, thanks to
65-
# http://mikewatkins.ca/2008/11/29/python-2-and-3-metaclasses/#using-the-metaclass-in-python-2-x-and-3-x
66-
_CStructParent = CStructMeta('_CStructParent', (object,), {})
67-
68-
69-
class AbstractCStruct(_CStructParent):
70-
def __init__(self, buffer=None, **kargs) -> None:
69+
class AbstractCStruct(metaclass=CStructMeta):
70+
__size__: int = 0
71+
__fields__: List[str] = []
72+
__fields_types__: Dict[str, FieldType]
73+
__byte_order__: Optional[str] = None
74+
__alignment__: int = 0
75+
__is_union__: bool = False
76+
77+
def __init__(
78+
self, buffer: Optional[Union[bytes, BinaryIO]] = None, flexible_array_length: Optional[int] = None, **kargs: Dict[str, Any]
79+
) -> None:
80+
self.set_flexible_array_length(flexible_array_length)
81+
self.__fields__ = [x for x in self.__fields__] # Create a copy
82+
self.__fields_types__ = OrderedDict({k: v.copy() for k, v in self.__fields_types__.items()}) # Create a copy
7183
if buffer is not None:
7284
self.unpack(buffer)
7385
else:
7486
try:
7587
self.unpack(buffer)
76-
except:
88+
except Exception:
7789
pass
7890
for key, value in kargs.items():
7991
setattr(self, key, value)
8092

8193
@classmethod
82-
def parse(cls, __struct__, __name__: Optional[str] = None, **kargs) -> Type[Any]:
94+
def parse(
95+
cls, __struct__: Union[str, Tokens, Dict[str, Any]], __name__: Optional[str] = None, **kargs: Dict[str, Any]
96+
) -> Type["AbstractCStruct"]:
8397
"""
8498
Return a new class mapping a C struct/union definition.
8599
@@ -89,31 +103,51 @@ def parse(cls, __struct__, __name__: Optional[str] = None, **kargs) -> Type[Any]
89103
:param __is_union__: (optional) True for union, False for struct (default)
90104
:returns: cls subclass
91105
"""
92-
kargs = dict(kargs)
93-
kargs['__struct__'] = __struct__
94-
if isinstance(__struct__, ("".__class__, u"".__class__, Tokens)):
95-
del kargs['__struct__']
96-
kargs.update(parse_def(__struct__, __cls__=cls, **kargs))
97-
kargs['__struct__'] = None
106+
cls_kargs: Dict[str, Any] = dict(kargs)
107+
cls_kargs['__struct__'] = __struct__
108+
if isinstance(__struct__, (str, Tokens)):
109+
del cls_kargs['__struct__']
110+
cls_kargs.update(parse_def(__struct__, __cls__=cls, **cls_kargs))
111+
cls_kargs['__struct__'] = None
112+
elif isinstance(__struct__, dict):
113+
del cls_kargs['__struct__']
114+
cls_kargs.update(__struct__)
115+
cls_kargs['__struct__'] = None
98116
if __name__ is None: # Anonymous struct
99117
__name__ = cls.__name__ + '_' + hashlib.sha1(str(__struct__).encode('utf-8')).hexdigest()
100-
kargs['__anonymous__'] = True
101-
kargs['__name__'] = __name__
102-
return type(__name__, (cls,), kargs)
118+
cls_kargs['__anonymous__'] = True
119+
cls_kargs['__name__'] = __name__
120+
return type(__name__, (cls,), cls_kargs)
103121

104-
def unpack(self, buffer: Optional[Union[bytes, BinaryIO]]) -> bool:
122+
def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> None:
123+
"""
124+
Set flexible array length (i.e. number of elements)
125+
126+
:flexible_array_length: flexible array length
127+
"""
128+
if flexible_array_length is not None:
129+
# Search for the flexible array
130+
flexible_array: Optional[FieldType] = [x for x in self.__fields_types__.values() if x.flexible_array][0]
131+
if flexible_array is None:
132+
raise ValueError("Flexible array not found in struct")
133+
flexible_array.vlen = flexible_array_length
134+
135+
def unpack(self, buffer: Optional[Union[bytes, BinaryIO]], flexible_array_length: Optional[int] = None) -> bool:
105136
"""
106137
Unpack bytes containing packed C structure data
107138
108139
:param buffer: bytes or binary stream to be unpacked
109140
"""
141+
self.set_flexible_array_length(flexible_array_length)
110142
if hasattr(buffer, 'read'):
111-
buffer = buffer.read(self.__size__)
143+
buffer = buffer.read(self.size) # type: ignore
112144
if not buffer:
113145
return False
114146
return self.unpack_from(buffer)
115147

116-
def unpack_from(self, buffer: Optional[bytes], offset: int = 0) -> bool: # pragma: no cover
148+
def unpack_from(
149+
self, buffer: Optional[bytes], offset: int = 0, flexible_array_length: Optional[int] = None
150+
) -> bool: # pragma: no cover
117151
"""
118152
Unpack bytes containing packed C structure data
119153
@@ -123,22 +157,35 @@ def unpack_from(self, buffer: Optional[bytes], offset: int = 0) -> bool: # prag
123157
raise NotImplementedError
124158

125159
def pack(self) -> bytes: # pragma: no cover
126-
"""
127-
Pack the structure data into bytes
128-
"""
160+
"Pack the structure data into bytes"
129161
raise NotImplementedError
130162

131163
def clear(self) -> None:
132164
self.unpack(None)
133165

134166
def __len__(self) -> int:
135-
"""Structure size (in bytes)"""
136-
return cast(int, self.__size__)
167+
"Actual structure size (in bytes)"
168+
return self.size
137169

138170
@property
139171
def size(self) -> int:
140-
"""Structure size (in bytes)"""
141-
return self.__size__
172+
"Actual structure size (in bytes)"
173+
if not self.__fields_types__: # no fields
174+
return 0
175+
elif self.__is_union__: # C union
176+
# Calculate the sizeof union as size of its largest element
177+
return max(x.vsize for x in self.__fields_types__.values())
178+
else: # C struct
179+
# Calculate the sizeof struct as last item's offset + size + padding
180+
last_field_type = list(self.__fields_types__.values())[-1]
181+
size = last_field_type.offset + last_field_type.vsize
182+
padding = calculate_padding(self.__byte_order__, self.__alignment__, size)
183+
return size + padding
184+
185+
@classmethod
186+
def sizeof(cls) -> int:
187+
"Structure size in bytes (flexible array member size is omitted)"
188+
return cls.__size__
142189

143190
def __eq__(self, other: Any) -> bool:
144191
return other is not None and isinstance(other, self.__class__) and self.__dict__ == other.__dict__

cstruct/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
# IN THE SOFTWARE.
2525
#
2626

27-
from typing import Any, Dict, Type
27+
from typing import Any, Dict, Type, TYPE_CHECKING
28+
29+
if TYPE_CHECKING:
30+
from .abstract import AbstractCStruct
2831

2932
__all__ = [
3033
'LITTLE_ENDIAN',
@@ -46,7 +49,7 @@
4649
# native order, size & alignment
4750
NATIVE_ORDER = '@'
4851

49-
STRUCTS: Dict[str, Type[Any]] = {}
52+
STRUCTS: Dict[str, Type["AbstractCStruct"]] = {}
5053

5154
DEFINES: Dict[str, Any] = {}
5255

0 commit comments

Comments
 (0)