From 683260935e03e23452c516a99fe7116b5930a3ab Mon Sep 17 00:00:00 2001 From: jonschz Date: Mon, 29 Jul 2024 18:18:11 +0200 Subject: [PATCH] Implement/fix Ghidra imports for multiple and virtual inheritance Unfortunately, the handling in Ghidra is still far from perfect. This is a good place to start, though. --- .../ghidra_scripts/lego_util/type_importer.py | 221 +++++++++++++++--- tools/isledecomp/isledecomp/cvdump/types.py | 85 ++++++- tools/isledecomp/tests/test_cvdump_types.py | 129 ++++++++++ 3 files changed, 398 insertions(+), 37 deletions(-) diff --git a/tools/ghidra_scripts/lego_util/type_importer.py b/tools/ghidra_scripts/lego_util/type_importer.py index c645ebf8..87d725ef 100644 --- a/tools/ghidra_scripts/lego_util/type_importer.py +++ b/tools/ghidra_scripts/lego_util/type_importer.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Callable, TypeVar +from typing import Any, Callable, Iterator, Optional, TypeVar # Disable spurious warnings in vscode / pylance # pyright: reportMissingModuleSource=false @@ -7,6 +7,7 @@ # pylint: disable=too-many-return-statements # a `match` would be better, but for now we are stuck with Python 3.9 # pylint: disable=no-else-return # Not sure why this rule even is a thing, this is great for checking exhaustiveness +from isledecomp.cvdump.types import VirtualBasePointer from lego_util.exceptions import ( ClassOrNamespaceNotFoundInGhidraError, TypeNotFoundError, @@ -56,10 +57,19 @@ def __init__(self, api: FlatProgramAPI, extraction: PdbFunctionExtractor): def types(self): return self.extraction.compare.cv.types - def import_pdb_type_into_ghidra(self, type_index: str) -> DataType: + def import_pdb_type_into_ghidra( + self, type_index: str, slim_for_vbase: bool = False + ) -> DataType: """ Recursively imports a type from the PDB into Ghidra. @param type_index Either a scalar type like `T_INT4(...)` or a PDB reference like `0x10ba` + @param slim_for_vbase If true, the current invocation + imports a superclass of some class where virtual inheritance is involved (directly or indirectly). + This case requires special handling: Let's say we have `class C: B` and `class B: virtual A`. Then cvdump + reports a size for B that includes both B's fields as well as the A contained at an offset within B, + which is not the correct structure to be contained in C. Therefore, we need to create a "slim" version of B + that fits inside C. + This value should always be `False` when the referenced type is not (a pointer to) a class. """ type_index_lower = type_index.lower() if type_index_lower.startswith("t_"): @@ -76,14 +86,19 @@ def import_pdb_type_into_ghidra(self, type_index: str) -> DataType: # follow forward reference (class, struct, union) if type_pdb.get("is_forward_ref", False): - return self._import_forward_ref_type(type_index_lower, type_pdb) + return self._import_forward_ref_type( + type_index_lower, type_pdb, slim_for_vbase + ) if type_category == "LF_POINTER": return add_pointer_type( - self.api, self.import_pdb_type_into_ghidra(type_pdb["element_type"]) + self.api, + self.import_pdb_type_into_ghidra( + type_pdb["element_type"], slim_for_vbase + ), ) elif type_category in ["LF_CLASS", "LF_STRUCTURE"]: - return self._import_class_or_struct(type_pdb) + return self._import_class_or_struct(type_pdb, slim_for_vbase) elif type_category == "LF_ARRAY": return self._import_array(type_pdb) elif type_category == "LF_ENUM": @@ -120,7 +135,10 @@ def _import_scalar_type(self, type_index_lower: str) -> DataType: return get_ghidra_type(self.api, scalar_cpp_type) def _import_forward_ref_type( - self, type_index, type_pdb: dict[str, Any] + self, + type_index, + type_pdb: dict[str, Any], + slim_for_vbase: bool = False, ) -> DataType: referenced_type = type_pdb.get("udt") or type_pdb.get("modifies") if referenced_type is None: @@ -136,7 +154,7 @@ def _import_forward_ref_type( type_index, referenced_type, ) - return self.import_pdb_type_into_ghidra(referenced_type) + return self.import_pdb_type_into_ghidra(referenced_type, slim_for_vbase) def _import_array(self, type_pdb: dict[str, Any]) -> DataType: inner_type = self.import_pdb_type_into_ghidra(type_pdb["array_type"]) @@ -182,12 +200,18 @@ def _import_enum(self, type_pdb: dict[str, Any]) -> DataType: return result - def _import_class_or_struct(self, type_in_pdb: dict[str, Any]) -> DataType: + def _import_class_or_struct( + self, + type_in_pdb: dict[str, Any], + slim_for_vbase: bool = False, + ) -> DataType: field_list_type: str = type_in_pdb["field_list_type"] field_list = self.types.keys[field_list_type.lower()] class_size: int = type_in_pdb["size"] class_name_with_namespace: str = sanitize_name(type_in_pdb["name"]) + if slim_for_vbase: + class_name_with_namespace += "::_vbase_slim" if class_name_with_namespace in self.handled_structs: logger.debug( @@ -205,11 +229,11 @@ def _import_class_or_struct(self, type_in_pdb: dict[str, Any]) -> DataType: self._get_or_create_namespace(class_name_with_namespace) - data_type = self._get_or_create_struct_data_type( + new_ghidra_struct = self._get_or_create_struct_data_type( class_name_with_namespace, class_size ) - if (old_size := data_type.getLength()) != class_size: + if (old_size := new_ghidra_struct.getLength()) != class_size: logger.warning( "Existing class %s had incorrect size %d. Setting to %d...", class_name_with_namespace, @@ -220,39 +244,172 @@ def _import_class_or_struct(self, type_in_pdb: dict[str, Any]) -> DataType: logger.info("Adding class data type %s", class_name_with_namespace) logger.debug("Class information: %s", type_in_pdb) - data_type.deleteAll() - data_type.growStructure(class_size) + components: list[dict[str, Any]] = [] + components.extend(self._get_components_from_base_classes(field_list)) + # can be missing when no new fields are declared + components.extend(self._get_components_from_members(field_list)) + components.extend( + self._get_components_from_vbase( + field_list, class_name_with_namespace, new_ghidra_struct + ) + ) + + components.sort(key=lambda c: c["offset"]) + + if slim_for_vbase: + # Make a "slim" version: shrink the size to the fields that are actually present. + # This makes a difference when the current class uses virtual inheritance + + assert ( + len(components) > 0 + ), f"Error: {class_name_with_namespace} should not be empty. There must be at least one direct or indirect vbase pointer." + last_component = components[-1] + class_size = last_component["offset"] + last_component["type"].getLength() + + self._overwrite_struct( + class_name_with_namespace, + new_ghidra_struct, + class_size, + components, + ) + + logger.info("Finished importing class %s", class_name_with_namespace) + + return new_ghidra_struct + + def _get_components_from_base_classes(self, field_list) -> Iterator[dict[str, Any]]: + non_virtual_base_classes: dict[str, int] = field_list.get("super", {}) + + for super_type, offset in non_virtual_base_classes.items(): + # If we have virtual inheritance _and_ a non-virtual base class here, we play safe and import slim version. + # This is technically not needed if only one of the superclasses uses virtual inheritance, but I am not aware of any instance. + import_slim_vbase_version_of_superclass = "vbase" in field_list + ghidra_type = self.import_pdb_type_into_ghidra( + super_type, slim_for_vbase=import_slim_vbase_version_of_superclass + ) + + yield { + "type": ghidra_type, + "offset": offset, + "name": "base" if offset == 0 else f"base_{ghidra_type.getName()}", + } + + def _get_components_from_members(self, field_list: dict[str, Any]): + members: list[dict[str, Any]] = field_list.get("members") or [] + for member in members: + yield member | {"type": self.import_pdb_type_into_ghidra(member["type"])} + + def _get_components_from_vbase( + self, + field_list: dict[str, Any], + class_name_with_namespace: str, + current_type: StructureInternal, + ) -> Iterator[dict[str, Any]]: + vbasepointer: Optional[VirtualBasePointer] = field_list.get("vbase", None) + + if vbasepointer is not None and any(x.direct for x in vbasepointer.bases): + vbaseptr_type = add_pointer_type( + self.api, + self._import_vbaseptr( + current_type, class_name_with_namespace, vbasepointer + ), + ) + yield { + "type": vbaseptr_type, + "offset": vbasepointer.vboffset, + "name": "vbase_offset", + } + + def _import_vbaseptr( + self, + current_type: StructureInternal, + class_name_with_namespace: str, + vbasepointer: VirtualBasePointer, + ) -> StructureInternal: + pointer_size = 4 + + components = [ + { + "offset": 0, + "type": add_pointer_type(self.api, current_type), + "name": "o_self", + } + ] + for vbase in vbasepointer.bases: + vbase_ghidra_type = self.import_pdb_type_into_ghidra(vbase.type) + + components.append( + { + "offset": vbase.index * pointer_size, + "type": add_pointer_type(self.api, vbase_ghidra_type), + "name": f"o_{vbase_ghidra_type.getName()}", + } + ) + + size = len(components) * pointer_size + + new_ghidra_struct = self._get_or_create_struct_data_type( + f"{class_name_with_namespace}::VBasePtr", size + ) + + self._overwrite_struct( + f"{class_name_with_namespace}::VBasePtr", + new_ghidra_struct, + size, + components, + ) + + return new_ghidra_struct + + def _overwrite_struct( + self, + class_name_with_namespace: str, + new_ghidra_struct: StructureInternal, + class_size: int, + components: list[dict[str, Any]], + ): + new_ghidra_struct.deleteAll() + new_ghidra_struct.growStructure(class_size) # this case happened e.g. for IUnknown, which linked to an (incorrect) existing library, and some other types as well. # Unfortunately, we don't get proper error handling for read-only types. # However, we really do NOT want to do this every time because the type might be self-referential and partially imported. - if data_type.getLength() != class_size: - data_type = self._delete_and_recreate_struct_data_type( - class_name_with_namespace, class_size, data_type + if new_ghidra_struct.getLength() != class_size: + new_ghidra_struct = self._delete_and_recreate_struct_data_type( + class_name_with_namespace, class_size, new_ghidra_struct ) - # can be missing when no new fields are declared - components: list[dict[str, Any]] = field_list.get("members") or [] - - super_type = field_list.get("super") - if super_type is not None: - components.insert(0, {"type": super_type, "offset": 0, "name": "base"}) - for component in components: - ghidra_type = self.import_pdb_type_into_ghidra(component["type"]) - logger.debug("Adding component to class: %s", component) + + offset: int = component["offset"] + logger.debug( + "Adding component %s to class: %s", component, class_name_with_namespace + ) try: - # for better logs - data_type.replaceAtOffset( - component["offset"], ghidra_type, -1, component["name"], None + # Make sure there is room for the new structure and that we have no collision. + existing_type = new_ghidra_struct.getComponentAt(offset) + assert ( + existing_type is not None + ), f"Struct collision: Offset {offset} in {class_name_with_namespace} is overlapped by another component" + + if existing_type.getDataType().getName() != "undefined": + # collision of structs beginning in the same place -> likely due to unions + logger.warning( + "Struct collision: Offset %d of %s already has a field (likely an inline union)", + offset, + class_name_with_namespace, + ) + + new_ghidra_struct.replaceAtOffset( + offset, + component["type"], + -1, # set to -1 for fixed-size components + component["name"], # name + None, # comment ) except Exception as e: - raise StructModificationError(type_in_pdb) from e - - logger.info("Finished importing class %s", class_name_with_namespace) - - return data_type + raise StructModificationError(class_name_with_namespace) from e def _get_or_create_namespace(self, class_name_with_namespace: str): colon_split = class_name_with_namespace.split("::") diff --git a/tools/isledecomp/isledecomp/cvdump/types.py b/tools/isledecomp/isledecomp/cvdump/types.py index b39ea248..f00f1ea1 100644 --- a/tools/isledecomp/isledecomp/cvdump/types.py +++ b/tools/isledecomp/isledecomp/cvdump/types.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass import re import logging from typing import Any, Dict, List, NamedTuple, Optional @@ -26,6 +27,19 @@ class FieldListItem(NamedTuple): type: str +@dataclass +class VirtualBaseClass: + type: str + index: int + direct: bool + + +@dataclass +class VirtualBasePointer: + vboffset: int + bases: list[VirtualBaseClass] + + class ScalarType(NamedTuple): offset: int name: Optional[str] @@ -157,6 +171,16 @@ class CvdumpTypesParser: r"^\s+list\[\d+\] = LF_BCLASS, (?P\w+), type = (?P.*), offset = (?P\d+)" ) + # LF_FIELDLIST virtual direct/indirect base pointer, line 1/2 + VBCLASS_RE = re.compile( + r"^\s+list\[\d+\] = LF_(?PI?)VBCLASS, .* base type = (?P.*)$" + ) + + # LF_FIELDLIST virtual direct/indirect base pointer, line 2/2 + VBCLASS_LINE_2_RE = re.compile( + r"^\s+virtual base ptr = .+, vbpoff = (?P\d+), vbind = (?P\d+)$" + ) + # LF_FIELDLIST member name (2/2) MEMBER_RE = re.compile(r"^\s+member name = '(?P.*)'$") @@ -282,12 +306,12 @@ def _get_field_list(self, type_obj: Dict[str, Any]) -> List[FieldListItem]: members: List[FieldListItem] = [] - super_id = field_obj.get("super") - if super_id is not None: + super_ids = field_obj.get("super", []) + for super_id in super_ids: # May need to resolve forward ref. superclass = self.get(super_id) if superclass.members is not None: - members = superclass.members + members += superclass.members raw_members = field_obj.get("members", []) members += [ @@ -526,7 +550,58 @@ def read_fieldlist_line(self, line: str): # Superclass is set here in the fieldlist rather than in LF_CLASS elif (match := self.SUPERCLASS_RE.match(line)) is not None: - self._set("super", normalize_type_id(match.group("type"))) + superclass_list: dict[str, int] = self.keys[self.last_key].setdefault( + "super", {} + ) + superclass_list[normalize_type_id(match.group("type"))] = int( + match.group("offset") + ) + + # virtual base class (direct or indirect) + elif (match := self.VBCLASS_RE.match(line)) is not None: + + virtual_base_pointer = self.keys[self.last_key].setdefault( + "vbase", + VirtualBasePointer( + vboffset=-1, # default to -1 until we parse the correct value + bases=[], + ), + ) + assert isinstance( + virtual_base_pointer, VirtualBasePointer + ) # type checker only + + virtual_base_pointer.bases.append( + VirtualBaseClass( + type=match.group("type"), + index=-1, # default to -1 until we parse the correct value + direct=match.group("indirect") != "I", + ) + ) + + elif (match := self.VBCLASS_LINE_2_RE.match(line)) is not None: + virtual_base_pointer = self.keys[self.last_key].get("vbase", None) + assert isinstance( + virtual_base_pointer, VirtualBasePointer + ), "Parsed the second line of an (I)VBCLASS without the first one" + vboffset = int(match.group("vboffset")) + + if virtual_base_pointer.vboffset == -1: + # default value + virtual_base_pointer.vboffset = vboffset + elif virtual_base_pointer.vboffset != vboffset: + # vboffset is always equal to 4 in our examples. We are not sure if there can be multiple + # virtual base pointers, and if so, how the layout is supposed to look. + # We therefore assume that there is always only one virtual base pointer. + logger.error( + "Unhandled: Found multiple virtual base pointers at offsets %d and %d", + virtual_base_pointer.vboffset, + vboffset, + ) + + virtual_base_pointer.bases[-1].index = int(match.group("vbindex")) + # these come out of order, and the lists are so short that it's fine to sort them every time + virtual_base_pointer.bases.sort(key=lambda x: x.index) # Member offset and type given on the first of two lines. elif (match := self.LIST_RE.match(line)) is not None: @@ -579,7 +654,7 @@ def read_arglist_line(self, line: str): else: logger.error("Unmatched line in arglist: %s", line[:-1]) - def read_pointer_line(self, line): + def read_pointer_line(self, line: str): if (match := self.LF_POINTER_ELEMENT.match(line)) is not None: self._set("element_type", match.group("element_type")) else: diff --git a/tools/isledecomp/tests/test_cvdump_types.py b/tools/isledecomp/tests/test_cvdump_types.py index 324870eb..3bd6afa0 100644 --- a/tools/isledecomp/tests/test_cvdump_types.py +++ b/tools/isledecomp/tests/test_cvdump_types.py @@ -6,6 +6,9 @@ CvdumpTypesParser, CvdumpKeyError, CvdumpIntegrityError, + FieldListItem, + VirtualBaseClass, + VirtualBasePointer, ) TEST_LINES = """ @@ -245,10 +248,111 @@ list[12] = LF_MEMBER, private, type = T_USHORT(0021), offset = 12 member name = 'm_length' + +0x4dee : Length = 406, Leaf = 0x1203 LF_FIELDLIST + list[0] = LF_VBCLASS, public, direct base type = 0x15EA + virtual base ptr = 0x43E9, vbpoff = 4, vbind = 3 + list[1] = LF_IVBCLASS, public, indirect base type = 0x1183 + virtual base ptr = 0x43E9, vbpoff = 4, vbind = 1 + list[2] = LF_IVBCLASS, public, indirect base type = 0x1468 + virtual base ptr = 0x43E9, vbpoff = 4, vbind = 2 + list[3] = LF_VFUNCTAB, type = 0x2B95 + list[4] = LF_ONEMETHOD, public, VANILLA, index = 0x15C2, name = 'LegoRaceMap' + list[5] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15C3, name = '~LegoRaceMap' + list[6] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15C5, name = 'Notify' + list[7] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15C4, name = 'ParseAction' + list[8] = LF_ONEMETHOD, public, VIRTUAL, index = 0x4DED, name = 'VTable0x70' + list[9] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x15C2, + vfptr offset = 0, name = 'FUN_1005d4b0' + list[10] = LF_MEMBER, private, type = T_UCHAR(0020), offset = 8 + member name = 'm_parentClass2Field1' + list[11] = LF_MEMBER, private, type = T_32PVOID(0403), offset = 12 + member name = 'm_parentClass2Field2' + +0x4def : Length = 34, Leaf = 0x1504 LF_CLASS + # members = 21, field list type 0x4dee, CONSTRUCTOR, + Derivation list type 0x0000, VT shape type 0x12a0 + Size = 436, class name = LegoRaceMap, UDT(0x00004def) + 0x4db6 : Length = 30, Leaf = 0x1504 LF_CLASS # members = 16, field list type 0x4db5, CONSTRUCTOR, OVERLOAD, Derivation list type 0x0000, VT shape type 0x1266 Size = 16, class name = MxString, UDT(0x00004db6) + +0x5591 : Length = 570, Leaf = 0x1203 LF_FIELDLIST + list[0] = LF_VBCLASS, public, direct base type = 0x15EA + virtual base ptr = 0x43E9, vbpoff = 4, vbind = 3 + list[1] = LF_IVBCLASS, public, indirect base type = 0x1183 + virtual base ptr = 0x43E9, vbpoff = 4, vbind = 1 + list[2] = LF_IVBCLASS, public, indirect base type = 0x1468 + virtual base ptr = 0x43E9, vbpoff = 4, vbind = 2 + list[3] = LF_VFUNCTAB, type = 0x4E11 + list[4] = LF_ONEMETHOD, public, VANILLA, index = 0x1ABD, name = 'LegoCarRaceActor' + list[5] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1AE0, name = 'ClassName' + list[6] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1AE1, name = 'IsA' + list[7] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1ADD, name = 'VTable0x6c' + list[8] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1ADB, name = 'VTable0x70' + list[9] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1ADA, name = 'SwitchBoundary' + list[10] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1ADC, name = 'VTable0x9c' + list[11] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x558E, + vfptr offset = 0, name = 'FUN_10080590' + list[12] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD8, + vfptr offset = 4, name = 'FUN_10012bb0' + list[13] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD9, + vfptr offset = 8, name = 'FUN_10012bc0' + list[14] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD8, + vfptr offset = 12, name = 'FUN_10012bd0' + list[15] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD9, + vfptr offset = 16, name = 'FUN_10012be0' + list[16] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD8, + vfptr offset = 20, name = 'FUN_10012bf0' + list[17] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD9, + vfptr offset = 24, name = 'FUN_10012c00' + list[18] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1ABD, + vfptr offset = 28, name = 'VTable0x1c' + list[19] = LF_MEMBER, protected, type = T_REAL32(0040), offset = 8 + member name = 'm_parentClass1Field1' + list[25] = LF_ONEMETHOD, public, VIRTUAL, (compgenx), index = 0x15D1, name = '~LegoCarRaceActor' + +0x5592 : Length = 38, Leaf = 0x1504 LF_CLASS + # members = 26, field list type 0x5591, CONSTRUCTOR, + Derivation list type 0x0000, VT shape type 0x34c7 + Size = 416, class name = LegoCarRaceActor, UDT(0x00005592) + +0x5593 : Length = 638, Leaf = 0x1203 LF_FIELDLIST + list[0] = LF_BCLASS, public, type = 0x5592, offset = 0 + list[1] = LF_BCLASS, public, type = 0x4DEF, offset = 32 + list[2] = LF_IVBCLASS, public, indirect base type = 0x1183 + virtual base ptr = 0x43E9, vbpoff = 4, vbind = 1 + list[3] = LF_IVBCLASS, public, indirect base type = 0x1468 + virtual base ptr = 0x43E9, vbpoff = 4, vbind = 2 + list[4] = LF_IVBCLASS, public, indirect base type = 0x15EA + virtual base ptr = 0x43E9, vbpoff = 4, vbind = 3 + list[5] = LF_ONEMETHOD, public, VANILLA, index = 0x15CD, name = 'LegoRaceCar' + list[6] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15CE, name = '~LegoRaceCar' + list[7] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15D2, name = 'Notify' + list[8] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15E8, name = 'ClassName' + list[9] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15E9, name = 'IsA' + list[10] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15D5, name = 'ParseAction' + list[11] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15D3, name = 'SetWorldSpeed' + list[12] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15DF, name = 'VTable0x6c' + list[13] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15D3, name = 'VTable0x70' + list[14] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15DC, name = 'VTable0x94' + list[15] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15E5, name = 'SwitchBoundary' + list[16] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15DD, name = 'VTable0x9c' + list[17] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x15D4, + vfptr offset = 32, name = 'SetMaxLinearVelocity' + list[18] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x15D4, + vfptr offset = 36, name = 'FUN_10012ff0' + list[19] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x5588, + vfptr offset = 40, name = 'HandleSkeletonKicks' + list[20] = LF_MEMBER, private, type = T_UCHAR(0020), offset = 84 + member name = 'm_childClassField' + +0x5594 : Length = 34, Leaf = 0x1504 LF_CLASS + # members = 30, field list type 0x5593, CONSTRUCTOR, + Derivation list type 0x0000, VT shape type 0x2d1e + Size = 512, class name = LegoRaceCar, UDT(0x000055bb) """ @@ -309,6 +413,31 @@ def test_members(parser: CvdumpTypesParser): (12, "m_length", "T_USHORT"), ] + # LegoRaceCar with multiple superclasses + assert parser.get("0x5594").members == [ + FieldListItem(offset=0, name="vftable", type="T_32PVOID"), + FieldListItem(offset=0, name="vftable", type="T_32PVOID"), + FieldListItem(offset=8, name="m_parentClass1Field1", type="T_REAL32"), + FieldListItem(offset=8, name="m_parentClass2Field1", type="T_UCHAR"), + FieldListItem(offset=12, name="m_parentClass2Field2", type="T_32PVOID"), + FieldListItem(offset=84, name="m_childClassField", type="T_UCHAR"), + ] + + +def test_virtual_base_classes(parser: CvdumpTypesParser): + """Make sure that virtual base classes are parsed correctly.""" + + lego_car_race_actor = parser.keys.get("0x5591") + assert lego_car_race_actor is not None + assert lego_car_race_actor["vbase"] == VirtualBasePointer( + vboffset=4, + bases=[ + VirtualBaseClass(type="0x1183", index=1, direct=False), + VirtualBaseClass(type="0x1468", index=2, direct=False), + VirtualBaseClass(type="0x15EA", index=3, direct=True), + ], + ) + def test_members_recursive(parser: CvdumpTypesParser): """Make sure that we unwrap the dependency tree correctly."""