From d7c7ecf86ed856486269cf13c1ffcadfe91f4f13 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 29 Jan 2025 10:09:12 +0100 Subject: [PATCH] Set types as attributes on the cstruct object --- dissect/cstruct/cstruct.py | 167 ++++++++++++++++++----------- dissect/cstruct/expression.py | 13 ++- dissect/cstruct/parser.py | 15 +-- dissect/cstruct/tools/stubgen.py | 40 ++++--- dissect/cstruct/types/structure.py | 4 +- tests/test_annotations.py | 5 +- tests/test_basic.py | 71 +++++++++--- tests/test_parser.py | 10 +- tests/test_tools_stubgen.py | 58 ++++++++++ 9 files changed, 263 insertions(+), 120 deletions(-) diff --git a/dissect/cstruct/cstruct.py b/dissect/cstruct/cstruct.py index e1633de2..0fb4300b 100644 --- a/dissect/cstruct/cstruct.py +++ b/dissect/cstruct/cstruct.py @@ -26,7 +26,6 @@ Void, Wchar, ) -from dissect.cstruct.types.base import MetaType if TYPE_CHECKING: from collections.abc import Iterable @@ -55,10 +54,12 @@ class cstruct: def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = None): self.endian = endian - self.consts = {} - self.includes = [] + self.consts: dict[str, int | str | bytes] = {} + self.types: dict[str, type[BaseType]] = {} + self.includes: list[str] = [] + # fmt: off - self.typedefs = { + initial_types = { # Internal types "int8": self._make_packed_type("int8", "b", int), "uint8": self._make_packed_type("uint8", "B", int), @@ -102,6 +103,22 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N "signed long long": "int64", "unsigned long long": "uint64", + # Other convenience types + "u8": "uint8", + "u16": "uint16", + "u32": "uint32", + "u64": "uint64", + "u128": "uint128", + "__u8": "uint8", + "__u16": "uint16", + "__u32": "uint32", + "__u64": "uint64", + "__u128": "uint128", + "uchar": "uint8", + "ushort": "uint16", + "uint": "uint32", + "ulong": "uint32", + # Windows types "BYTE": "uint8", "CHAR": "char", @@ -169,25 +186,12 @@ def __init__(self, load: str = "", *, endian: str = "<", pointer: str | None = N "_DWORD": "uint32", "_QWORD": "uint64", "_OWORD": "uint128", - - # Other convenience types - "u8": "uint8", - "u16": "uint16", - "u32": "uint32", - "u64": "uint64", - "u128": "uint128", - "__u8": "uint8", - "__u16": "uint16", - "__u32": "uint32", - "__u64": "uint64", - "__u128": "uint128", - "uchar": "uint8", - "ushort": "uint16", - "uint": "uint32", - "ulong": "uint32", } # fmt: on + for name, type_ in initial_types.items(): + self.add_type(name, type_) + pointer = pointer or ("uint64" if sys.maxsize > 2**32 else "uint32") self.pointer: type[BaseType] = self.resolve(pointer) self._anonymous_count = 0 @@ -202,27 +206,25 @@ def __getattr__(self, attr: str) -> Any: pass try: - return self.resolve(self.typedefs[attr]) + return self.types[attr] except KeyError: pass - raise AttributeError(f"Invalid attribute: {attr}") + raise AttributeError(f"'{type(self).__name__}' object has no attribute {attr!r}") def __copy__(self) -> cstruct: cs = cstruct(endian=self.endian, pointer=self.pointer.__name__) cs._anonymous_count = self._anonymous_count - cs.consts = self.consts.copy() cs.includes = self.includes.copy() - # Update typedefs to point to the new cstruct instance - for name, type in self.typedefs.items(): - if name in cs.typedefs: - continue + # Update types to point to the new cstruct instance + for name, type_ in self.types.items(): + new_type = copy.copy(type_) + new_type.cs = cs + cs.add_type(name, new_type, replace=True) - if isinstance(type, MetaType): - new_type = copy.copy(type) - new_type.cs = cs - cs.typedefs[name] = new_type + for name, value in self.consts.items(): + cs.add_const(name, value) return cs @@ -231,22 +233,34 @@ def _next_anonymous(self) -> str: self._anonymous_count += 1 return name + def _add_attr(self, name: str, value: Any) -> None: + # Names that collide with the cstruct class (e.g. a struct named ``load``) are not set as attributes + # to avoid breaking the instance. They remain accessible through ``resolve`` and the type dicts. + if name in _RESERVED_NAMES: + return + setattr(self, name, value) + def add_type(self, name: str, type_: type[BaseType] | str, replace: bool = False) -> None: - """Add a type or type reference. + """Add a type or type alias. Only use this method when creating type aliases or adding already bound types. + All types will be resolved to their actual type objects prior to being added. Args: name: Name of the type to be added. type_: The type to be added. Can be a str reference to another type or a compatible type class. + If a str is given, it will be resolved to the actual type object. + replace: Whether to replace the type if it already exists. Raises: ValueError: If the type already exists. """ - if not replace and (name in self.typedefs and self.resolve(self.typedefs[name]) != self.resolve(type_)): + typeobj = self.resolve(type_) + if not replace and (existing := self.types.get(name)) is not None and existing is not typeobj: raise ValueError(f"Duplicate type: {name}") - self.typedefs[name] = type_ + self.types[name] = typeobj + self._add_attr(name, typeobj) addtype = add_type @@ -267,6 +281,28 @@ def add_custom_type( """ self.add_type(name, self._make_type(name, (type_,), size, alignment=alignment, attrs=kwargs)) + def add_const(self, name: str, value: Any) -> None: + """Add a constant value. + + Args: + name: Name of the constant to be added. + value: The value of the constant. + """ + self.consts[name] = value + self._add_attr(name, value) + + def del_const(self, name: str) -> None: + """Delete a constant value. + + Args: + name: Name of the constant to be deleted. + + Raises: + KeyError: If the constant does not exist. + """ + del self.consts[name] + self.__dict__.pop(name, None) + def load(self, definition: str, deftype: int | None = None, **kwargs) -> cstruct: """Parse structures from the given definitions using the given definition type. @@ -324,8 +360,8 @@ def cdef(self) -> str: if defines: blocks.append("\n".join(defines)) - for name, typedef in self.typedefs.items(): - if name in empty.typedefs or not isinstance(typedef, type): + for name, typedef in self.types.items(): + if name in empty.types or not isinstance(typedef, type): continue if not issubclass(typedef, (Structure, Enum, Flag)): @@ -365,20 +401,13 @@ def resolve(self, name: type[BaseType] | str) -> type[BaseType]: Raises: ResolveError: If the type can't be resolved. """ - type_name = name - if not isinstance(type_name, str): - return type_name + if not isinstance(name, str): + return name - for _ in range(10): - if type_name not in self.typedefs: - raise ResolveError(f"Unknown type {name}") - - type_name = self.typedefs[type_name] - - if not isinstance(type_name, str): - return type_name - - raise ResolveError(f"Recursion limit exceeded while resolving type {name}") + try: + return self.types[name] + except KeyError: + raise ResolveError(f"Unknown type {name}") from None def copy(self) -> cstruct: """Create a copy of this cstruct instance. @@ -555,6 +584,21 @@ class void(Void): ... # signed long long: TypeAlias = int64 # unsigned long long: TypeAlias = uint64 + u8: TypeAlias = uint8 + u16: TypeAlias = uint16 + u32: TypeAlias = uint32 + u64: TypeAlias = uint64 + u128: TypeAlias = uint128 + __u8: TypeAlias = uint8 + __u16: TypeAlias = uint16 + __u32: TypeAlias = uint32 + __u64: TypeAlias = uint64 + __u128: TypeAlias = uint128 + uchar: TypeAlias = uint8 + ushort: TypeAlias = uint16 + uint: TypeAlias = uint32 + ulong: TypeAlias = uint32 + BYTE: TypeAlias = uint8 CHAR: TypeAlias = char SHORT: TypeAlias = int16 @@ -620,20 +664,17 @@ class void(Void): ... _QWORD: TypeAlias = uint64 _OWORD: TypeAlias = uint128 - u8: TypeAlias = uint8 - u16: TypeAlias = uint16 - u32: TypeAlias = uint32 - u64: TypeAlias = uint64 - u128: TypeAlias = uint128 - __u8: TypeAlias = uint8 - __u16: TypeAlias = uint16 - __u32: TypeAlias = uint32 - __u64: TypeAlias = uint64 - __u128: TypeAlias = uint128 - uchar: TypeAlias = uint8 - ushort: TypeAlias = uint16 - uint: TypeAlias = uint32 - ulong: TypeAlias = uint32 + +# Attribute names that types and constants may never shadow: the public API and methods of the cstruct +# class itself, plus the instance attributes created in __init__. +_RESERVED_NAMES = frozenset(dir(cstruct)) | { + "endian", + "consts", + "types", + "includes", + "pointer", + "_anonymous_count", +} def ctypes(structure: type[Structure]) -> type[_ctypes.Structure]: diff --git a/dissect/cstruct/expression.py b/dissect/cstruct/expression.py index f8cfd43c..6e2058bc 100644 --- a/dissect/cstruct/expression.py +++ b/dissect/cstruct/expression.py @@ -12,6 +12,8 @@ from dissect.cstruct import cstruct from dissect.cstruct.lexer import Token +_MISSING = object() + BINARY_OPERATORS: dict[TokenType, Callable[[int, int], int]] = { TokenType.PIPE: lambda a, b: a | b, TokenType.CARET: lambda a, b: a ^ b, @@ -164,16 +166,13 @@ def evaluate(self, cs: cstruct, context: dict[str, int] | None = None) -> int: obj = context[part] elif part in cs.consts: obj = cs.consts[part] - elif part in cs.typedefs: - obj = cs.resolve(part) + elif part in cs.types: + obj = cs.types[part] else: raise self._error(f"Unknown identifier: '{ident}'", token=token) else: - if isinstance(obj, dict) and part in obj: - obj = obj[part] - elif hasattr(obj, part): - obj = getattr(obj, part) - else: + obj = obj.get(part, _MISSING) if isinstance(obj, dict) else getattr(obj, part, _MISSING) + if obj is _MISSING: raise self._error(f"Unknown identifier: '{ident}'", token=token) try: diff --git a/dissect/cstruct/parser.py b/dissect/cstruct/parser.py index d6ef2941..fe8353a1 100644 --- a/dissect/cstruct/parser.py +++ b/dissect/cstruct/parser.py @@ -150,7 +150,8 @@ def _parse(self) -> None: # If it's an anonymous enum/flag, add its members to the constants for convenience if not type_.__name__: - self.cs.consts.update(type_.__members__) + for k, v in type_.__members__.items(): + self.cs.add_const(k, v) self._expect(TokenType.SEMICOLON) else: @@ -201,17 +202,17 @@ def _parse_define(self) -> None: # If evaluation fails, just keep it as a string (e.g. for macro-like constants) pass - self.cs.consts[name_token.value] = value + self.cs.add_const(name_token.value, value) def _parse_undef(self) -> None: """Parse an undef directive and remove the constant.""" self._expect(TokenType.PP_UNDEF) name_token = self._expect(TokenType.IDENTIFIER) - if name_token.value in self.cs.consts: - del self.cs.consts[name_token.value] - else: - raise self._error(f"constant {name_token.value!r} not defined", token=name_token) + try: + self.cs.del_const(name_token.value) + except KeyError: + raise self._error(f"constant {name_token.value!r} not defined", token=name_token) from None def _parse_include(self) -> None: """Parse an include directive and add the included file to the includes list.""" @@ -544,7 +545,7 @@ def _parse_type_spec(self) -> type[BaseType]: ): # This identifier is followed by a field delimiter, it should be the field name, # UNLESS the current parts don't form a valid type yet. - if " ".join(parts) in self.cs.typedefs: + if " ".join(parts) in self.cs.types: break # Current parts don't resolve, consume and hope this completes the type name diff --git a/dissect/cstruct/tools/stubgen.py b/dissect/cstruct/tools/stubgen.py index 02aaebb3..a21f740c 100644 --- a/dissect/cstruct/tools/stubgen.py +++ b/dissect/cstruct/tools/stubgen.py @@ -79,31 +79,29 @@ def generate_cstruct_stub(cs: cstruct, module_prefix: str = "", cls_name: str = defined_names = set() - # Then typedefs - for name, typedef in cs.typedefs.items(): - if name in empty_cs.typedefs: + # Then types + for name, type_ in cs.types.items(): + if name in empty_cs.types: continue - if typedef.__name__ in empty_cs.typedefs: - stub = f"{name}: TypeAlias = {cs_prefix}{typedef.__name__}" - elif typedef.__name__ in defined_names: + if type_.__name__ in empty_cs.types: + stub = f"{name}: TypeAlias = {cs_prefix}{type_.__name__}" + elif type_.__name__ in defined_names: # Create an alias to the type if we have already seen it before. - stub = f"{name}: TypeAlias = {typedef.__name__}" - elif issubclass(typedef, (types.Enum, types.Flag)): - stub = generate_enum_stub(typedef, cs_prefix=cs_prefix, module_prefix=module_prefix) - elif issubclass(typedef, types.Pointer): - typehint = generate_typehint(typedef, prefix=cs_prefix, module_prefix=module_prefix) + stub = f"{name}: TypeAlias = {type_.__name__}" + elif issubclass(type_, (types.Enum, types.Flag)): + stub = generate_enum_stub(type_, cs_prefix=cs_prefix, module_prefix=module_prefix) + elif issubclass(type_, types.Pointer): + typehint = generate_typehint(type_, prefix=cs_prefix, module_prefix=module_prefix) stub = f"{name}: TypeAlias = {typehint}" - elif issubclass(typedef, types.Structure): - stub = generate_structure_stub(typedef, cs_prefix=cs_prefix, module_prefix=module_prefix) - elif issubclass(typedef, types.BaseType): - stub = generate_generic_stub(typedef, cs_prefix=cs_prefix, module_prefix=module_prefix) - elif isinstance(typedef, str): - stub = f"{name}: TypeAlias = {typedef}" + elif issubclass(type_, types.Structure): + stub = generate_structure_stub(type_, cs_prefix=cs_prefix, module_prefix=module_prefix) + elif issubclass(type_, types.BaseType): + stub = generate_generic_stub(type_, cs_prefix=cs_prefix, module_prefix=module_prefix) else: - raise TypeError(f"Unknown typedef: {typedef}") + raise TypeError(f"Unknown type: {type_}") - defined_names.add(typedef.__name__) + defined_names.add(type_.__name__) body.append(textwrap.indent(stub, prefix=indent)) @@ -158,12 +156,10 @@ def generate_structure_stub( module_prefix: str = "", ) -> str: result = [f"class {name_prefix}{structure.__name__}({module_prefix}{structure.__base__.__name__}):"] - indent = " " * 4 args = ["self"] for field_name, field in structure.fields.items(): - type_name = field.type.__name__ inlined = False # If it's a structure and not globally defined, add an inline stub for it @@ -171,7 +167,7 @@ def generate_structure_stub( while issubclass(nested_type, types.BaseArray): nested_type = nested_type.type - if issubclass(nested_type, types.Structure) and type_name not in structure.cs.typedefs: + if issubclass(nested_type, types.Structure) and nested_type.__name__ not in structure.cs.types: inlined = True inline_stub = generate_structure_stub(nested_type, cs_prefix=cs_prefix, module_prefix=module_prefix) diff --git a/dissect/cstruct/types/structure.py b/dissect/cstruct/types/structure.py index 348e5d66..8641d5c5 100644 --- a/dissect/cstruct/types/structure.py +++ b/dissect/cstruct/types/structure.py @@ -309,7 +309,7 @@ def _write(cls, stream: BinaryIO, data: Structure) -> int: num = 0 for field in cls.__fields__: - field_type = cls.cs.resolve(field.type) + field_type = field.type bit_field_type = ( (field_type.type if isinstance(field_type, EnumMetaType) else field_type) if field.bits else None @@ -614,7 +614,7 @@ def _read_fields( buf = io.BytesIO(stream.read(cls.size)) for field in cls.__fields__: - field_type = cls.cs.resolve(field.type) + field_type = field.type start = 0 if field.offset is not None: diff --git a/tests/test_annotations.py b/tests/test_annotations.py index a135e1de..97230266 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -8,12 +8,15 @@ from dissect.cstruct.cstruct import cstruct from dissect.cstruct.types.base import BaseType +CS = cstruct() + @pytest.mark.parametrize( "name", - [name for name in cstruct().typedefs if " " not in name], + [name for name in CS.types if " " not in name], ) def test_cstruct_type_annotation(name: str, monkeypatch: pytest.MonkeyPatch) -> None: + """Verify that all default types defined in cstruct have type annotations.""" with ( patch("typing.TYPE_CHECKING", True), patch("dissect.cstruct.types.base.MetaType.__getitem__", lambda self, item: self), diff --git a/tests/test_basic.py b/tests/test_basic.py index bfa46180..3e75f210 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -37,7 +37,7 @@ def test_load_file(cs: cstruct, compiled: bool, tmp_path: Path) -> None: tmp_path.joinpath("testdef.txt").write_text(textwrap.dedent(cdef)) cs.loadfile(tmp_path.joinpath("testdef.txt"), compiled=compiled) - assert "test" in cs.typedefs + assert "test" in cs.types def test_load_init() -> None: @@ -49,12 +49,12 @@ def test_load_init() -> None: """ # load with first positional argument cs = cstruct(cdef) - assert "test" in cs.typedefs + assert "test" in cs.types assert cs.endian == "<" # load from keyword argument and big endian cs = cstruct(load=cdef, endian=">") - assert "test" in cs.typedefs + assert "test" in cs.types a = cs.test(a=0xBADC0DE, b=0xACCE55ED) assert len(bytes(a)) == 12 assert bytes(a) == a.dumps() @@ -62,7 +62,7 @@ def test_load_init() -> None: # load using positional argument and little endian cs = cstruct(cdef, endian="<") - assert "test" in cs.typedefs + assert "test" in cs.types a = cs.test(a=0xBADC0DE, b=0xACCE55ED) assert len(bytes(a)) == 12 assert bytes(a) == a.dumps() @@ -81,7 +81,7 @@ def test_load_init_kwargs_only() -> None: cs = cstruct(cdef, ">") cs = cstruct(cdef, endian=">") - assert "test" in cs.typedefs + assert "test" in cs.types assert cs.endian == ">" @@ -96,11 +96,10 @@ def test_type_resolve(cs: cstruct) -> None: cs.resolve("fake") cs.add_type("ref0", "uint32") - for i in range(1, 15): # Recursion limit is currently 10 + for i in range(1, 15): cs.add_type(f"ref{i}", f"ref{i - 1}") - with pytest.raises(ResolveError, match="Recursion limit exceeded"): - cs.resolve("ref14") + assert cs.resolve("ref14") is cs.uint32 def test_constants(cs: cstruct) -> None: @@ -140,6 +139,52 @@ def test_duplicate_types(cs: cstruct) -> None: cs.load("""typedef uint64 Test;""") +def test_type_replace(cs: cstruct) -> None: + cs.add_type("BYTE", "uint16", replace=True) + assert cs.resolve("BYTE") is cs.uint16 + assert cs.BYTE is cs.uint16 + + # Adding a conflicting type over an existing alias should raise + cs.add_type("foo", "uint8") + with pytest.raises(ValueError, match="Duplicate type"): + cs.load("struct foo { uint32 x; };") + + +def test_reserved_attribute_names(cs: cstruct) -> None: + cs.load("struct load { uint8 a; };") + assert callable(cs.load) + assert cs.resolve("load").fields["a"].type is cs.uint8 + + cs.load("struct consts { uint8 a; };") + assert isinstance(cs.consts, dict) + + cs.load("#define resolve 1") + assert callable(cs.resolve) + assert cs.consts["resolve"] == 1 + + +def test_del_const_after_copy(cs: cstruct) -> None: + cs.load("#define FOO 1") + cs_copy = cs.copy() + + assert cs_copy.FOO == 1 + cs_copy.del_const("FOO") + assert "FOO" not in cs_copy.consts + assert cs.FOO == 1 + + +def test_const_type_name_collision(cs: cstruct) -> None: + # When a constant and a type share a name, the last one added wins on attribute access + cs.load("#define INT 1") + assert cs.INT == 1 + assert cs.resolve("INT") is cs.int32 + + cs.load("struct foo { uint8 a; };") + cs.load("#define foo 1") + assert cs.foo == 1 + assert cs.resolve("foo").fields["a"].type is cs.uint8 + + def test_typedef(cs: cstruct) -> None: cdef = """ typedef uint32 test; @@ -445,7 +490,7 @@ def test_reserved_keyword(cs: cstruct, compiled: bool) -> None: cs.load(cdef, compiled=compiled) for name in ["in", "class", "for"]: - assert name in cs.typedefs + assert name in cs.types assert verify_compiled(cs.resolve(name), compiled) assert cs.resolve(name)(b"\x01").a == 1 @@ -542,11 +587,11 @@ def test_copy(cs: cstruct) -> None: }; """) - assert "test" in cs.typedefs - assert "test2" not in cs.typedefs + assert "test" in cs.types + assert "test2" not in cs.types - assert "test" in cs_copy.typedefs - assert "test2" in cs_copy.typedefs + assert "test" in cs_copy.types + assert "test2" in cs_copy.types # Verify that types in the copied cstruct reference the copied cstruct assert cs_copy.resolve("test").cs is cs_copy diff --git a/tests/test_parser.py b/tests/test_parser.py index 9299e8a4..54273f08 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -155,7 +155,7 @@ def test_struct_names(cs: cstruct) -> None: """ cs.load(cdef) - assert all(c in cs.typedefs for c in ("a", "b", "c", "d", "e")) + assert all(c in cs.types for c in ("a", "b", "c", "d", "e")) assert cs.a.__name__ == "a" # For convenience, unnamed structs get the same name as their typedef if they have one @@ -319,7 +319,7 @@ def test_conditional_ifdef(cs: cstruct) -> None: """ cs.load(cdef) - assert "test" in cs.typedefs + assert "test" in cs.types def test_conditional_ifndef(cs: cstruct) -> None: @@ -349,7 +349,7 @@ def test_conditional_ifndef_guard(cs: cstruct) -> None: cs.load(cdef) assert "__MYGUARD" in cs.consts - assert "myStruct" in cs.typedefs + assert "myStruct" in cs.types def test_conditional_nested() -> None: @@ -396,7 +396,7 @@ def test_conditional_in_struct(cs: cstruct) -> None: """ cs.load(cdef) - assert "t_bitfield" in cs.typedefs + assert "t_bitfield" in cs.types assert "fval" in cs.t_bitfield.fields assert "bit0" in cs.t_bitfield.fields["fval"].type.fields assert "bit1" in cs.t_bitfield.fields["fval"].type.fields @@ -450,7 +450,7 @@ def test_multiple_declarators(cs: cstruct) -> None: """ cs.load(cdef) - assert "test" in cs.typedefs + assert "test" in cs.types assert all(field in cs.test.fields for field in ("a", "b", "c", "d", "e")) assert cs.test.fields["a"].type == cs.uint32 assert cs.test.fields["b"].type == cs.uint32 diff --git a/tests/test_tools_stubgen.py b/tests/test_tools_stubgen.py index 21c7b272..2675cba9 100644 --- a/tests/test_tools_stubgen.py +++ b/tests/test_tools_stubgen.py @@ -339,6 +339,64 @@ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... """, id="pointer-alias", ), + pytest.param( + """ + struct A { + uint8 a; + }; + + struct Test { + A x; + }; + """, + """ + class cstruct(cstruct): + class A(Structure): + a: cstruct.uint8 + @overload + def __init__(self, a: cstruct.uint8 | None = ...): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + + class Test(Structure): + x: cstruct.A + @overload + def __init__(self, x: cstruct.A | None = ...): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + + """, + id="reference other struct", + ), + pytest.param( + """ + struct A { + uint8 a; + }; + + struct Test { + A x[4]; + }; + """, + """ + class cstruct(cstruct): + class A(Structure): + a: cstruct.uint8 + @overload + def __init__(self, a: cstruct.uint8 | None = ...): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + + class Test(Structure): + x: Array[cstruct.A] + @overload + def __init__(self, x: Array[cstruct.A] | None = ...): ... + @overload + def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ... + + """, + id="reference other struct array", + ), ], ) def test_generate_cstruct_stub(cs: cstruct, cdef: str, expected: str) -> None: