mirror of
https://github.com/EricPlayZ/EGameTools.git
synced 2025-07-18 17:37:53 +08:00
backup script changes for JSONParser
This commit is contained in:
@ -1,216 +1,61 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
from typing import Optional, List, get_type_hints
|
||||
from prodict import Prodict
|
||||
|
||||
class ParsedParam(Prodict):
|
||||
type: str
|
||||
name: str
|
||||
parsedClassParam: Optional["ParsedClass"]
|
||||
|
||||
from ExportClassH import Utils, HeaderGen
|
||||
def init(self):
|
||||
self.type = ""
|
||||
self.name = ""
|
||||
self.parsedClassParam = None
|
||||
|
||||
virtualFuncPlaceholderCounter: int = 0 # Counter for placeholder virtual functions
|
||||
virtualFuncDuplicateCounter: dict[str, int] = {} # Counter for duplicate virtual functions
|
||||
class ParsedClass(Prodict):
|
||||
type: str
|
||||
parentNamespaces: List[str]
|
||||
parentClasses: List[str]
|
||||
name: str
|
||||
templateParams: List["ParsedParam"]
|
||||
fullClassName: str
|
||||
childClasses: List["ParsedClass"]
|
||||
functions: List["ParsedFunction"]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParsedFunction:
|
||||
"""Parse a demangled function signature and return an instance."""
|
||||
fullFuncSig: str = ""
|
||||
type: str = ""
|
||||
access: str = ""
|
||||
returnType: Optional[ClassName] = None
|
||||
className: Optional[ClassName] = None
|
||||
funcName: str = ""
|
||||
params: list[ClassName] = field(default_factory=list[ClassName])
|
||||
const: bool = False
|
||||
def init(self):
|
||||
self.type = ""
|
||||
self.parentNamespaces = []
|
||||
self.parentClasses = []
|
||||
self.name = ""
|
||||
self.templateParams = []
|
||||
self.fullClassName = ""
|
||||
self.childClasses = []
|
||||
self.functions = []
|
||||
|
||||
def __init__(self, signature: str, onlyVirtualFuncs: bool):
|
||||
global virtualFuncPlaceholderCounter
|
||||
global virtualFuncDuplicateCounter
|
||||
class ParsedFunction(Prodict):
|
||||
type: str
|
||||
funcType: str
|
||||
access: str
|
||||
returnTypes: List[ParsedParam]
|
||||
parentNamespaces: List[str]
|
||||
parentClasses: List[str]
|
||||
funcName: str
|
||||
params: List
|
||||
const: bool
|
||||
fullFuncSig: str
|
||||
|
||||
object.__setattr__(self, "fullFuncSig", signature)
|
||||
object.__setattr__(self, "type", "")
|
||||
object.__setattr__(self, "access", "")
|
||||
object.__setattr__(self, "returnType", None)
|
||||
object.__setattr__(self, "className", None)
|
||||
object.__setattr__(self, "funcName", "")
|
||||
object.__setattr__(self, "params", [])
|
||||
object.__setattr__(self, "const", False)
|
||||
|
||||
signature = signature.strip()
|
||||
|
||||
isDuplicateFunc: bool = False
|
||||
isIDAGeneratedType: bool = False
|
||||
isIDAGeneratedTypeParsed: bool = False
|
||||
if (signature.startswith("DUPLICATE_FUNC")):
|
||||
isDuplicateFunc = True
|
||||
signature = signature.removeprefix("DUPLICATE_FUNC").strip()
|
||||
if (signature.startswith("IDA_GEN_TYPE")):
|
||||
isIDAGeneratedType = True
|
||||
signature = signature.removeprefix("IDA_GEN_TYPE").strip()
|
||||
elif (signature.startswith("IDA_GEN_PARSED")):
|
||||
isIDAGeneratedTypeParsed = True
|
||||
signature = signature.removeprefix("IDA_GEN_PARSED").strip()
|
||||
|
||||
access: str = ""
|
||||
for keyword in ("public:", "protected:", "private:"):
|
||||
if signature.startswith(keyword):
|
||||
access = keyword[:-1] # remove the colon
|
||||
signature = signature[len(keyword):].strip()
|
||||
break
|
||||
|
||||
# Find parameters and const qualifier
|
||||
paramsOpenParenIndex: int = signature.find('(')
|
||||
paramsCloseParenIndex: int = signature.rfind(')')
|
||||
|
||||
if paramsOpenParenIndex != -1 and paramsCloseParenIndex != -1:
|
||||
params: str = signature[paramsOpenParenIndex + 1:paramsCloseParenIndex]
|
||||
paramsStrList: list[str] = Utils.SplitByCommaOutsideTemplates(params)
|
||||
paramsList: list[ClassName] = [ClassName(paramStr) for paramStr in paramsStrList if paramStr]
|
||||
|
||||
remainingInputBeforeParamsParen: str = signature[:paramsOpenParenIndex].strip()
|
||||
remainingInputAfterParamsParen: str = signature[paramsCloseParenIndex + 1:].strip()
|
||||
const: str = "const" if "const" in remainingInputAfterParamsParen else ""
|
||||
|
||||
returnType: str = ""
|
||||
classAndFuncName: str = ""
|
||||
className: str = ""
|
||||
funcName: str = ""
|
||||
if not isIDAGeneratedType:
|
||||
# Find the last space outside of angle brackets
|
||||
lastSpaceIndex: int = -1
|
||||
lastClassSeparatorIndex: int = -1
|
||||
|
||||
templateDepth: int = 0
|
||||
for i in range(len(remainingInputBeforeParamsParen)):
|
||||
if remainingInputBeforeParamsParen[i] == '<':
|
||||
templateDepth += 1
|
||||
elif remainingInputBeforeParamsParen[i] == '>':
|
||||
templateDepth -= 1
|
||||
elif templateDepth == 0 and remainingInputBeforeParamsParen[i] == ' ':
|
||||
lastSpaceIndex = i
|
||||
|
||||
if lastSpaceIndex != -1:
|
||||
# Split at the last space outside angle brackets
|
||||
returnType = remainingInputBeforeParamsParen[:lastSpaceIndex].strip()
|
||||
classAndFuncName = remainingInputBeforeParamsParen[lastSpaceIndex+1:].strip()
|
||||
|
||||
templateDepth = 0
|
||||
# Find the last class separator outside of angle brackets
|
||||
for i in range(len(classAndFuncName)):
|
||||
if classAndFuncName[i] == '<':
|
||||
templateDepth += 1
|
||||
elif classAndFuncName[i] == '>':
|
||||
templateDepth -= 1
|
||||
elif templateDepth == 0 and classAndFuncName[i:i+2] == '::':
|
||||
lastClassSeparatorIndex = i
|
||||
|
||||
if lastClassSeparatorIndex != -1:
|
||||
className = classAndFuncName[:lastClassSeparatorIndex]
|
||||
funcName = classAndFuncName[lastClassSeparatorIndex+2:]
|
||||
else:
|
||||
className = "::".join(classAndFuncName.split("::")[:-1])
|
||||
funcName = classAndFuncName.split("::")[-1]
|
||||
else:
|
||||
templateDepth = 0
|
||||
# Find the last class separator outside of angle brackets
|
||||
for i in range(len(remainingInputBeforeParamsParen)):
|
||||
if remainingInputBeforeParamsParen[i] == '<':
|
||||
templateDepth += 1
|
||||
elif remainingInputBeforeParamsParen[i] == '>':
|
||||
templateDepth -= 1
|
||||
elif templateDepth == 0 and remainingInputBeforeParamsParen[i:i+2] == '::':
|
||||
lastClassSeparatorIndex = i
|
||||
|
||||
if lastClassSeparatorIndex != -1:
|
||||
classAndFuncName: str = remainingInputBeforeParamsParen
|
||||
className: str = classAndFuncName[:lastClassSeparatorIndex]
|
||||
funcName: str = classAndFuncName[lastClassSeparatorIndex+2:]
|
||||
else:
|
||||
returnType = remainingInputBeforeParamsParen
|
||||
|
||||
if isDuplicateFunc:
|
||||
if signature not in virtualFuncDuplicateCounter:
|
||||
virtualFuncDuplicateCounter[signature] = 0
|
||||
virtualFuncDuplicateCounter[signature] += 1
|
||||
funcName = f"_{funcName}{virtualFuncDuplicateCounter[signature]}"
|
||||
|
||||
if onlyVirtualFuncs:
|
||||
returnType = returnType.replace("static", "").strip()
|
||||
if isIDAGeneratedType or isIDAGeneratedTypeParsed or isDuplicateFunc or "virtual" not in returnType:
|
||||
type = "basic_vfunc"
|
||||
else:
|
||||
type = "vfunc"
|
||||
else:
|
||||
if "virtual" not in returnType:
|
||||
type = "func"
|
||||
elif isIDAGeneratedType or isIDAGeneratedTypeParsed or isDuplicateFunc:
|
||||
type = "basic_vfunc"
|
||||
else:
|
||||
type = "vfunc"
|
||||
object.__setattr__(self, "type", type)
|
||||
object.__setattr__(self, "access", access if access else "public")
|
||||
object.__setattr__(self, "returnType", ClassName(returnType) if returnType else None)
|
||||
object.__setattr__(self, "className", ClassName(className) if className else None)
|
||||
object.__setattr__(self, "funcName", funcName)
|
||||
object.__setattr__(self, "params", paramsList)
|
||||
object.__setattr__(self, "const", bool(const))
|
||||
return
|
||||
|
||||
# Generate a simple virtual void function
|
||||
if onlyVirtualFuncs and signature == "_purecall":
|
||||
virtualFuncPlaceholderCounter += 1
|
||||
object.__setattr__(self, "type", "stripped_vfunc")
|
||||
object.__setattr__(self, "access", access if access else "public")
|
||||
object.__setattr__(self, "returnType", ClassName("virtual void"))
|
||||
object.__setattr__(self, "funcName", f"_StrippedVFunc{virtualFuncPlaceholderCounter}")
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParsedClassVar:
|
||||
"""Parse a demangled global class var signature and return an instance."""
|
||||
access: str = ""
|
||||
varType: Optional[ClassName] = None
|
||||
className: Optional[ClassName] = None
|
||||
varName: str = ""
|
||||
|
||||
def __init__(self, signature: str):
|
||||
# Initialize defaults.
|
||||
object.__setattr__(self, "access", "")
|
||||
object.__setattr__(self, "varType", None)
|
||||
object.__setattr__(self, "className", None)
|
||||
object.__setattr__(self, "varName", "")
|
||||
|
||||
# Extract access specifier.
|
||||
access = ""
|
||||
for keyword in ("public:", "protected:", "private:"):
|
||||
if signature.startswith(keyword):
|
||||
access = keyword[:-1] # remove the colon
|
||||
signature = signature[len(keyword):].strip()
|
||||
break
|
||||
|
||||
# For class variables, we expect no parameters (i.e. no parentheses).
|
||||
if signature.find('(') == -1 and signature.rfind(')') == -1:
|
||||
# Use a backward search to find the last space outside templates.
|
||||
last_space = Utils.FindLastSpaceOutsideTemplates(signature)
|
||||
if last_space != -1:
|
||||
varType = signature[:last_space].strip()
|
||||
classAndVarName = signature[last_space+1:].strip()
|
||||
else:
|
||||
# If no space, assume there's no varType
|
||||
varType = ""
|
||||
classAndVarName = signature
|
||||
|
||||
# Find the last "::" separator outside templates.
|
||||
last_sep = Utils.FindLastClassSeparatorOutsideTemplates(classAndVarName)
|
||||
if last_sep != -1:
|
||||
class_name_str = classAndVarName[:last_sep].strip()
|
||||
var_name = classAndVarName[last_sep+2:].strip()
|
||||
else:
|
||||
# Fallback: if there are "::" tokens, split them; otherwise, take entire string as varName.
|
||||
parts = classAndVarName.split("::")
|
||||
if len(parts) > 1:
|
||||
class_name_str = "::".join(parts[:-1]).strip()
|
||||
var_name = parts[-1].strip()
|
||||
else:
|
||||
class_name_str = ""
|
||||
var_name = classAndVarName.strip()
|
||||
|
||||
object.__setattr__(self, "access", access if access else "public")
|
||||
object.__setattr__(self, "varType", ClassName(varType) if varType else None)
|
||||
object.__setattr__(self, "className", ClassName(class_name_str) if class_name_str else None)
|
||||
object.__setattr__(self, "varName", var_name)
|
||||
return
|
||||
def init(self):
|
||||
self.type = "function"
|
||||
self.funcType = "function"
|
||||
self.access = "public"
|
||||
self.returnTypes = []
|
||||
self.parentNamespaces = []
|
||||
self.parentClasses = []
|
||||
self.funcName = ""
|
||||
self.params = []
|
||||
self.const = False
|
||||
self.fullFuncSig = ""
|
||||
|
||||
ParsedParam.__annotations__ = get_type_hints(ParsedParam)
|
||||
ParsedParam.__annotations__["parsedClassParam"] = ParsedClass
|
||||
ParsedClass.__annotations__ = get_type_hints(ParsedClass)
|
||||
ParsedFunction.__annotations__ = get_type_hints(ParsedFunction)
|
@ -1,20 +1,20 @@
|
||||
import os
|
||||
import pickle
|
||||
import idc
|
||||
from typing import Optional
|
||||
|
||||
from ExportClassH import Utils, Config, RTTIAnalyzer
|
||||
from ExportClassH.ClassDefs import ParsedClass, ParsedFunction
|
||||
|
||||
# Global caches
|
||||
parsedClassVarsByClass: dict[str, list[dict]] = {} # Cache of parsed class vars by class name
|
||||
parsedVTableFuncsByClass: dict[str, list[dict]] = {} # Cache of parsed functions by class name
|
||||
parsedFuncsByClass: dict[str, list[dict]] = {} # Cache of parsed functions by class name
|
||||
allParsedFuncs: list[dict] = []
|
||||
parsedClassVarsByClass: dict[str, list[ParsedClass]] = {} # Cache of parsed class vars by class name
|
||||
parsedVTableFuncsByClass: dict[str, list[ParsedFunction]] = {} # Cache of parsed functions by class name
|
||||
parsedFuncsByClass: dict[str, list[ParsedFunction]] = {} # Cache of parsed functions by class name
|
||||
allParsedFuncs: list[ParsedFunction] = []
|
||||
unparsedExportedSigs: list[str] = []
|
||||
allClassVarsAreParsed = False # Flag to indicate if all class vars have been parsed
|
||||
allFuncsAreParsed = False # Flag to indicate if all functions have been parsed
|
||||
|
||||
def IsClassGenerable(cls: dict) -> bool:
|
||||
def IsClassGenerable(cls: ParsedClass) -> bool:
|
||||
"""
|
||||
Check if a class has any parsable elements (class vars, vtable functions, regular functions).
|
||||
Returns True if the class is generable, False if it should be treated as a namespace.
|
||||
@ -52,19 +52,19 @@ def GetClassTypeFromParsedSigs(targetClass: ClassName, allParsedElements: tuple[
|
||||
# Check class vars first
|
||||
for parsedClassVar in parsedClassVars:
|
||||
if (parsedClassVar.varType and
|
||||
parsedClassVar.varType.namespacedClassedName == targetClass.namespacedClassedName and
|
||||
parsedClassVar.varType.fullClassStr == targetClass.fullClassStr and
|
||||
parsedClassVar.varType.type):
|
||||
return parsedClassVar.varType.type
|
||||
# Check vtable functions next
|
||||
for parsedVTFunc in parsedVtFuncs:
|
||||
if (parsedVTFunc.returnType and
|
||||
parsedVTFunc.returnType.namespacedClassedName == targetClass.namespacedClassedName and
|
||||
parsedVTFunc.returnType.fullClassStr == targetClass.fullClassStr and
|
||||
parsedVTFunc.returnType.type):
|
||||
return parsedVTFunc.returnType.type
|
||||
# Check all parsed functions last
|
||||
for parsedFunc in allParsedFuncs:
|
||||
if (parsedFunc.returnType and
|
||||
parsedFunc.returnType.namespacedClassedName == targetClass.namespacedClassedName and
|
||||
parsedFunc.returnType.fullClassStr == targetClass.fullClassStr and
|
||||
parsedFunc.returnType.type):
|
||||
return parsedFunc.returnType.type
|
||||
|
||||
@ -76,23 +76,6 @@ def ComputeUnparsedExportedSigs(demangledExportedSigs: list[str], parsedSigs: li
|
||||
# Then, for each exported signature, check if it appears in the big string.
|
||||
return [sig for sig in demangledExportedSigs if sig not in big_parsed]
|
||||
|
||||
def GetDemangledExportedSigs() -> list[str]:
|
||||
"""
|
||||
Generate a list of demangled function signatures from IDA's database.
|
||||
Uses a set to avoid duplicate entries.
|
||||
"""
|
||||
sigs_set = set()
|
||||
entry_qty = idc.get_entry_qty()
|
||||
for i in range(entry_qty):
|
||||
ea: int = idc.get_entry(i)
|
||||
exportedSig: str = idc.get_func_name(ea) or idc.get_name(ea)
|
||||
if not exportedSig:
|
||||
continue
|
||||
demangledExportedSig: str = Utils.DemangleSig(exportedSig)
|
||||
if demangledExportedSig and "~" not in demangledExportedSig:
|
||||
sigs_set.add(demangledExportedSig)
|
||||
return list(sigs_set)
|
||||
|
||||
def GetParsedClassVars(targetClass: dict = {}) -> list[ParsedClassVar]:
|
||||
"""
|
||||
Collect and parse all class var signatures from the IDA database.
|
||||
@ -133,7 +116,7 @@ def GetParsedClassVars(targetClass: dict = {}) -> list[ParsedClassVar]:
|
||||
print(f"Failed parsing class var sig: \"{sig}\"")
|
||||
continue
|
||||
|
||||
parsedClassVarsByClass.setdefault(parsedVar.className.namespacedClassedName, []).append(parsedVar)
|
||||
parsedClassVarsByClass.setdefault(parsedVar.className.fullClassStr, []).append(parsedVar)
|
||||
|
||||
allClassVarsAreParsed = True
|
||||
|
||||
@ -153,9 +136,9 @@ def GetParsedClassVars(targetClass: dict = {}) -> list[ParsedClassVar]:
|
||||
if targetClass is None:
|
||||
return [var for vars_list in parsedClassVarsByClass.values() for var in vars_list]
|
||||
else:
|
||||
return parsedClassVarsByClass.get(targetClass.namespacedClassedName, [])
|
||||
return parsedClassVarsByClass.get(targetClass.fullClassStr, [])
|
||||
|
||||
def GetParsedVTableFuncs(targetClass: ClassName) -> list[ParsedFunction]:
|
||||
def GetParsedVTableFuncs(targetClass: ParsedClass) -> list[ParsedFunction]:
|
||||
"""
|
||||
Collect and parse all function signatures from the IDA database.
|
||||
If target_class is provided, only return functions for that class.
|
||||
@ -164,27 +147,27 @@ def GetParsedVTableFuncs(targetClass: ClassName) -> list[ParsedFunction]:
|
||||
global parsedVTableFuncsByClass
|
||||
|
||||
if targetClass not in parsedVTableFuncsByClass:
|
||||
parsedVTableFuncsByClass[targetClass.namespacedClassedName] = []
|
||||
parsedVTableFuncsByClass[targetClass.fullClassStr] = []
|
||||
|
||||
for (demangledFuncSig, rawType) in RTTIAnalyzer.GetDemangledVTableFuncSigs(targetClass):
|
||||
if rawType:
|
||||
parsedFunc: ParsedFunction = ParsedFunction(rawType, True)
|
||||
if parsedFunc.returnType:
|
||||
newParamTypes: str = CreateParamNamesForVTFunc(parsedFunc, True) if parsedFunc.params else ""
|
||||
demangledFuncSig = f"{'DUPLICATE_FUNC ' if demangledFuncSig.startswith('DUPLICATE_FUNC') else ''}IDA_GEN_PARSED virtual {parsedFunc.returnType.namespacedClassedName} {demangledFuncSig.removeprefix('DUPLICATE_FUNC').strip()}({newParamTypes})"
|
||||
demangledFuncSig = f"{'DUPLICATE_FUNC ' if demangledFuncSig.startswith('DUPLICATE_FUNC') else ''}IDA_GEN_PARSED virtual {parsedFunc.returnType.fullClassStr} {demangledFuncSig.removeprefix('DUPLICATE_FUNC').strip()}({newParamTypes})"
|
||||
elif demangledFuncSig.startswith("DUPLICATE_FUNC"):
|
||||
parsedFunc: ParsedFunction = ParsedFunction(demangledFuncSig.removeprefix("DUPLICATE_FUNC").strip(), True)
|
||||
if parsedFunc.returnType:
|
||||
newParamTypes: str = CreateParamNamesForVTFunc(parsedFunc, False) if parsedFunc.params else ""
|
||||
demangledFuncSig = f"DUPLICATE_FUNC {parsedFunc.returnType.namespacedClassedName} {parsedFunc.funcName}({newParamTypes})"
|
||||
demangledFuncSig = f"DUPLICATE_FUNC {parsedFunc.returnType.fullClassStr} {parsedFunc.funcName}({newParamTypes})"
|
||||
|
||||
parsedFunc: ParsedFunction = ParsedFunction(demangledFuncSig, True)
|
||||
if not parsedFunc.className:
|
||||
object.__setattr__(parsedFunc, "className", targetClass)
|
||||
|
||||
parsedVTableFuncsByClass[targetClass.namespacedClassedName].append(parsedFunc)
|
||||
parsedVTableFuncsByClass[targetClass.fullClassStr].append(parsedFunc)
|
||||
|
||||
return parsedVTableFuncsByClass.get(targetClass.namespacedClassedName, [])
|
||||
return parsedVTableFuncsByClass.get(targetClass.fullClassStr, [])
|
||||
|
||||
def GetParsedFuncs(targetClass: Optional[ClassName] = None) -> list[ParsedFunction]:
|
||||
"""
|
||||
@ -216,7 +199,7 @@ def GetParsedFuncs(targetClass: Optional[ClassName] = None) -> list[ParsedFuncti
|
||||
if not parsedFunc.type or not parsedFunc.className:
|
||||
print(f"Failed parsing func sig: \"{demangledFuncSig}\"")
|
||||
continue
|
||||
parsedFuncsByClass.setdefault(parsedFunc.className.namespacedClassedName, []).append(parsedFunc)
|
||||
parsedFuncsByClass.setdefault(parsedFunc.className.fullClassStr, []).append(parsedFunc)
|
||||
allFuncsAreParsed = True
|
||||
try:
|
||||
os.makedirs(Config.CACHE_OUTPUT_PATH, exist_ok=True)
|
||||
@ -232,23 +215,23 @@ def GetParsedFuncs(targetClass: Optional[ClassName] = None) -> list[ParsedFuncti
|
||||
if targetClass is None:
|
||||
return [pf for funcList in parsedFuncsByClass.values() for pf in funcList]
|
||||
else:
|
||||
return parsedFuncsByClass.get(targetClass.namespacedClassedName, [])
|
||||
return parsedFuncsByClass.get(targetClass.fullClassStr, [])
|
||||
|
||||
def GetAllParsedClassVarsAndFuncs(cls: dict) -> tuple[list[dict], list[dict], list[dict]]:
|
||||
def GetAllParsedClassVarsAndFuncs(cls: ParsedClass) -> tuple[list[ParsedClass], list[ParsedClass], list[ParsedClass]]:
|
||||
global allParsedFuncs
|
||||
|
||||
parsedVTableClassFuncs: list[dict] = GetParsedVTableFuncs(cls)
|
||||
parsedVTableClassFuncs: list[ParsedFunction] = GetParsedVTableFuncs(cls)
|
||||
if not parsedVTableClassFuncs:
|
||||
print(f"No matching VTable function signatures were found for {cls.namespacedClassedName}.")
|
||||
print(f"No matching VTable function signatures were found for {cls.fullClassStr}.")
|
||||
|
||||
parsedClassFuncs: list[dict] = GetParsedFuncs(cls)
|
||||
parsedClassFuncs: list[ParsedFunction] = GetParsedFuncs(cls)
|
||||
if not parsedClassFuncs:
|
||||
print(f"No matching function signatures were found for {cls.namespacedClassedName}.")
|
||||
print(f"No matching function signatures were found for {cls.fullClassStr}.")
|
||||
allParsedFuncs = GetParsedFuncs()
|
||||
|
||||
parsedClassVars: list[dict] = GetParsedClassVars(cls)
|
||||
if not parsedClassVars:
|
||||
print(f"No matching class var signatures were found for {cls.namespacedClassedName}.")
|
||||
print(f"No matching class var signatures were found for {cls.fullClassStr}.")
|
||||
|
||||
# Get non-vtable methods
|
||||
vTableFuncsSet: set[str] = {pf.fullFuncSig for pf in parsedVTableClassFuncs}
|
||||
|
39
_IDAScripts/ExportClassH/IDAUtils.py
Normal file
39
_IDAScripts/ExportClassH/IDAUtils.py
Normal file
@ -0,0 +1,39 @@
|
||||
import idc
|
||||
|
||||
from ExportClassH import Utils
|
||||
|
||||
# def GetDemangledExportedSigs() -> list[str]:
|
||||
# """
|
||||
# Generate a list of demangled function signatures from IDA's database.
|
||||
# Uses a set to avoid duplicate entries.
|
||||
# """
|
||||
# sigs_set = set()
|
||||
# entry_qty = idc.get_entry_qty()
|
||||
# for i in range(entry_qty):
|
||||
# ea: int = idc.get_entry(i)
|
||||
# exportedSig: str = idc.get_func_name(ea) or idc.get_name(ea)
|
||||
# if not exportedSig:
|
||||
# continue
|
||||
# demangledExportedSig: str = Utils.DemangleSig(exportedSig)
|
||||
# if demangledExportedSig and "~" not in demangledExportedSig and not demangledExportedSig.endswith("::$TSS0") and "::`vftable'" not in demangledExportedSig:
|
||||
# sigs_set.add(demangledExportedSig)
|
||||
# return list(sigs_set)
|
||||
|
||||
def GetDemangledExportedSigs() -> list[str]:
|
||||
"""
|
||||
Generate a list of demangled function signatures from IDA's database.
|
||||
Uses a set to avoid duplicate entries based on (ordinal, signature).
|
||||
"""
|
||||
sigs_set = set()
|
||||
entry_qty = idc.get_entry_qty()
|
||||
for i in range(entry_qty):
|
||||
ea: int = idc.get_entry(i)
|
||||
exportedSig: str = idc.get_func_name(ea) or idc.get_name(ea)
|
||||
exportedSig = idc.get_entry_name(i)
|
||||
if not exportedSig:
|
||||
continue
|
||||
demangledExportedSig: str = Utils.DemangleSig(exportedSig)
|
||||
if demangledExportedSig and "~" not in demangledExportedSig and not demangledExportedSig.endswith("::$TSS0") and "::`vftable'" not in demangledExportedSig:
|
||||
sigs_set.add((i + 1, demangledExportedSig))
|
||||
|
||||
return [sig for _, sig in sorted(sigs_set)]
|
@ -1,57 +1,80 @@
|
||||
from ExportClassH import Utils, ClassParser
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
def SplitTypeFromName(fullName: str) -> tuple[str, str]:
|
||||
CLASS_TYPES = ["class", "struct", "enum", "union"]
|
||||
FUNC_QUALIFIERS = ["virtual", "static", "inline", "explicit", "friend"]
|
||||
|
||||
from ExportClassH import Utils, IDAUtils
|
||||
from ExportClassH.ClassDefs import ParsedClass, ParsedFunction, ParsedParam
|
||||
|
||||
CLASS_TYPES = ["namespace", "class", "struct", "enum", "union"]
|
||||
FUNC_TYPES = ["function", "strippedVirtual", "basicVirtual", "virtual"]
|
||||
TYPES_OF_RETURN_TYPES = ["returnType", "classReturnType"]
|
||||
STD_CLASSES = ["std", "rapidjson"]
|
||||
|
||||
parsedClassesDict: dict[str, ParsedClass] = {}
|
||||
|
||||
def GetTypeAndNameStr(fullName: str) -> str:
|
||||
parts = Utils.ExtractTypeTokensFromString(fullName)
|
||||
if not parts:
|
||||
return "", ""
|
||||
return ""
|
||||
if not len(parts) > 1:
|
||||
return ""
|
||||
|
||||
if len(parts) > 1:
|
||||
if parts[0] in CLASS_TYPES:
|
||||
return parts[0], parts[1]
|
||||
elif len(parts) > 2 and parts[0] in FUNC_QUALIFIERS and parts[1] in CLASS_TYPES:
|
||||
return parts[1], parts[2]
|
||||
|
||||
return "", fullName
|
||||
for i in range(len(parts) - 1):
|
||||
if parts[i] in CLASS_TYPES:
|
||||
return f"{parts[i]} {parts[i + 1]}"
|
||||
return ""
|
||||
|
||||
def ExtractClassNameAndTemplateParams(templatedClassName: str) -> tuple[str, list[str]]:
|
||||
templateParams = []
|
||||
def SplitTypeFromName(fullName: str) -> tuple[str, str]:
|
||||
typeAndNameStr = GetTypeAndNameStr(fullName)
|
||||
if not typeAndNameStr:
|
||||
return "", fullName
|
||||
|
||||
typeAndName = typeAndNameStr.split(maxsplit=1)
|
||||
return typeAndName[0], typeAndName[1]
|
||||
|
||||
def GetParsedParamsFromList(paramsList: list[str], type: str) -> list[ParsedParam]:
|
||||
params: list[ParsedParam] = []
|
||||
for i in range(len(paramsList)):
|
||||
typeOfParam: str = type
|
||||
classType, className = SplitTypeFromName(paramsList[i])
|
||||
nameOfParam: str = className
|
||||
parsedClassOfParam: Optional[ParsedClass] = None
|
||||
|
||||
if classType:
|
||||
typeOfParam = f"class{typeOfParam[0].upper()}{typeOfParam[1:]}"
|
||||
parsedClassOfParam = ParseClassStr(f"{classType} {className}")
|
||||
params.append(ParsedParam(type=typeOfParam, name=nameOfParam, parsedClassParam=parsedClassOfParam))
|
||||
return params
|
||||
|
||||
def ExtractClassNameAndTemplateParams(templatedClassName: str) -> tuple[str, list[ParsedParam]]:
|
||||
className = templatedClassName
|
||||
templateParams: list[ParsedParam] = []
|
||||
|
||||
templateOpen = templatedClassName.find('<')
|
||||
templateClose = templatedClassName.rfind('>')
|
||||
|
||||
if templateOpen != -1 and templateClose != -1:
|
||||
className = templatedClassName[:templateOpen].strip()
|
||||
paramsStr = templatedClassName[templateOpen + 1:templateClose].strip()
|
||||
paramsStr = Utils.ReplaceIDATypes(paramsStr)
|
||||
paramsStr = Utils.CleanType(paramsStr)
|
||||
|
||||
# Split by commas, but only those outside of nested templates
|
||||
templateParams = Utils.SplitByCommaOutsideTemplates(paramsStr)
|
||||
templateParams = GetParsedParamsFromList(Utils.SplitByCommaOutsideTemplates(paramsStr), "templateParam")
|
||||
|
||||
return className, templateParams
|
||||
|
||||
def ParseClassStr(clsStr: str) -> dict:
|
||||
classInfo = {
|
||||
"type": "",
|
||||
"name": "",
|
||||
"templateParams": [],
|
||||
"parentNamespace": [],
|
||||
"parentClass": []
|
||||
}
|
||||
|
||||
# Strip whitespace
|
||||
def ParseClassStr(clsStr: str) -> Optional[ParsedClass]:
|
||||
clsStr = clsStr.strip()
|
||||
if not clsStr:
|
||||
return {}
|
||||
return None
|
||||
|
||||
parsedClass = ParsedClass()
|
||||
|
||||
# Extract type (struct, class, etc.)
|
||||
typeAndName = SplitTypeFromName(clsStr)
|
||||
if not typeAndName[0]:
|
||||
return {}
|
||||
return None
|
||||
|
||||
classInfo["type"] = typeAndName[0]
|
||||
parsedClass.type = typeAndName[0]
|
||||
templatedClassNameWithNS = typeAndName[1]
|
||||
|
||||
# Split into namespaced parts and the final class name with templates
|
||||
@ -62,45 +85,33 @@ def ParseClassStr(clsStr: str) -> dict:
|
||||
if lastClassSeparatorIndex != -1:
|
||||
namespacesAndClasses = templatedClassNameWithNS[:lastClassSeparatorIndex].strip()
|
||||
templatedClassName = templatedClassNameWithNS[lastClassSeparatorIndex+2:].strip()
|
||||
else:
|
||||
templatedClassName = templatedClassNameWithNS
|
||||
|
||||
# Extract template parameters
|
||||
className, templateParams = ExtractClassNameAndTemplateParams(templatedClassName)
|
||||
classInfo["name"] = className
|
||||
classInfo["templateParams"] = templateParams
|
||||
|
||||
# Split namespaces and classes - for this example we'll use a simple approach
|
||||
# where we assume the first part(s) are namespaces and later parts are parent classes
|
||||
if namespacesAndClasses:
|
||||
allParts = namespacesAndClasses.split("::")
|
||||
continueOnlyWithClasses: bool = False
|
||||
for part in allParts:
|
||||
if not ClassParser.IsClassGenerable(classInfo) and not continueOnlyWithClasses:
|
||||
classInfo["parentNamespace"].append(part)
|
||||
else:
|
||||
if not continueOnlyWithClasses:
|
||||
continueOnlyWithClasses = True
|
||||
classInfo["parentClass"].append(part)
|
||||
|
||||
return classInfo
|
||||
parsedClass.name = className
|
||||
parsedClass.templateParams = templateParams
|
||||
parentNamespaces = Utils.SplitByClassSeparatorOutsideTemplates(namespacesAndClasses)
|
||||
if any(STD_CLASS in parentNamespaces for STD_CLASS in STD_CLASSES):
|
||||
return None
|
||||
parsedClass.parentNamespaces.extend(parentNamespaces)
|
||||
|
||||
parsedClass.fullClassName = f"{'::'.join(parsedClass.parentNamespaces + parsedClass.parentClasses + [parsedClass.name])}"
|
||||
return parsedClass
|
||||
|
||||
virtualFuncDuplicateCounter: dict[str, int] = {}
|
||||
virtualFuncPlaceholderCounter: dict[str, int] = {}
|
||||
def ParseFuncStr(funcStr: str, onlyVirtualFuncs: bool = False) -> Optional[ParsedFunction]:
|
||||
global virtualFuncDuplicateCounter
|
||||
global virtualFuncPlaceholderCounter
|
||||
|
||||
def ParseFuncStr(funcStr: str, onlyVirtualFuncs: bool = False) -> dict:
|
||||
funcInfo = {
|
||||
"type": "function",
|
||||
"access": "public",
|
||||
"funcType": "",
|
||||
"returnType": {},
|
||||
"funcName": "",
|
||||
"params": [],
|
||||
"const": False,
|
||||
"parentNamespace": [],
|
||||
"parentClass": [],
|
||||
"fullFuncSig": funcStr.strip()
|
||||
}
|
||||
|
||||
# Strip whitespace
|
||||
funcStr = funcStr.strip()
|
||||
if not funcStr:
|
||||
return {}
|
||||
return None
|
||||
|
||||
parsedFunc = ParsedFunction(fullFuncSig=funcStr.strip())
|
||||
|
||||
# Handle special cases
|
||||
isDuplicateFunc = False
|
||||
@ -120,7 +131,7 @@ def ParseFuncStr(funcStr: str, onlyVirtualFuncs: bool = False) -> dict:
|
||||
# Extract access modifier
|
||||
for keyword in ("public:", "protected:", "private:"):
|
||||
if funcStr.startswith(keyword):
|
||||
funcInfo["access"] = keyword[:-1] # remove the colon
|
||||
parsedFunc.access = keyword[:-1] # remove the colon
|
||||
funcStr = funcStr[len(keyword):].strip()
|
||||
break
|
||||
|
||||
@ -142,11 +153,11 @@ def ParseFuncStr(funcStr: str, onlyVirtualFuncs: bool = False) -> dict:
|
||||
finalParamsStrList.append(parsedParamAsClass)
|
||||
else:
|
||||
finalParamsStrList.append(paramStr)
|
||||
funcInfo["params"] = finalParamsStrList
|
||||
parsedFunc.params = finalParamsStrList
|
||||
|
||||
# Check for const qualifier
|
||||
remainingInputAfterParamsParen = funcStr[paramsCloseParenIndex + 1:].strip()
|
||||
funcInfo["const"] = "const" in remainingInputAfterParamsParen
|
||||
parsedFunc.const = "const" in remainingInputAfterParamsParen
|
||||
|
||||
# Process everything before parameters
|
||||
remainingInputBeforeParamsParen = funcStr[:paramsOpenParenIndex].strip()
|
||||
@ -165,11 +176,11 @@ def ParseFuncStr(funcStr: str, onlyVirtualFuncs: bool = False) -> dict:
|
||||
lastClassSeparatorIndex = Utils.FindLastClassSeparatorOutsideTemplates(classAndFuncName)
|
||||
|
||||
if lastClassSeparatorIndex != -1:
|
||||
className = classAndFuncName[:lastClassSeparatorIndex]
|
||||
namespacesAndClasses = classAndFuncName[:lastClassSeparatorIndex]
|
||||
funcName = classAndFuncName[lastClassSeparatorIndex+2:]
|
||||
else:
|
||||
classParts = classAndFuncName.split("::")
|
||||
className = "::".join(classParts[:-1]) if len(classParts) > 1 else ""
|
||||
namespacesAndClasses = "::".join(classParts[:-1]) if len(classParts) > 1 else ""
|
||||
funcName = classParts[-1]
|
||||
else:
|
||||
# No space found, try to find class separator
|
||||
@ -177,47 +188,220 @@ def ParseFuncStr(funcStr: str, onlyVirtualFuncs: bool = False) -> dict:
|
||||
|
||||
if lastClassSeparatorIndex != -1:
|
||||
classAndFuncName = remainingInputBeforeParamsParen
|
||||
className = classAndFuncName[:lastClassSeparatorIndex]
|
||||
namespacesAndClasses = classAndFuncName[:lastClassSeparatorIndex]
|
||||
funcName = classAndFuncName[lastClassSeparatorIndex+2:]
|
||||
else:
|
||||
returnType = ""
|
||||
funcName = remainingInputBeforeParamsParen
|
||||
className = ""
|
||||
namespacesAndClasses = ""
|
||||
else:
|
||||
returnType = remainingInputBeforeParamsParen
|
||||
className = ""
|
||||
namespacesAndClasses = ""
|
||||
funcName = ""
|
||||
|
||||
# parentNamespaces, parentClasses = ExtractParentNamespacesAndClasses(namespacesAndClasses)
|
||||
# funcInfo.parentNamespaces.extend(parentNamespaces)
|
||||
# funcInfo.parentNamespaces.extend(parentClasses)
|
||||
|
||||
# Handle duplicate function naming
|
||||
if isDuplicateFunc:
|
||||
funcName = f"_{funcName}1" # Simplified counter handling
|
||||
if funcStr not in virtualFuncDuplicateCounter:
|
||||
virtualFuncDuplicateCounter[funcStr] = 0
|
||||
virtualFuncDuplicateCounter[funcStr] += 1
|
||||
funcName = f"_{funcName}{virtualFuncDuplicateCounter[funcStr]}"
|
||||
|
||||
# Determine function type
|
||||
if onlyVirtualFuncs:
|
||||
returnType = returnType.replace("static", "").strip()
|
||||
if isIDAGeneratedType or isIDAGeneratedTypeParsed or isDuplicateFunc or "virtual" not in returnType:
|
||||
funcInfo["funcType"] = "basicVirtual"
|
||||
parsedFunc.funcType = "basicVirtual"
|
||||
else:
|
||||
funcInfo["funcType"] = "virtual"
|
||||
parsedFunc.funcType = "virtual"
|
||||
else:
|
||||
if "virtual" not in returnType:
|
||||
funcInfo["funcType"] = "func"
|
||||
parsedFunc.funcType = "function"
|
||||
elif isIDAGeneratedType or isIDAGeneratedTypeParsed or isDuplicateFunc:
|
||||
funcInfo["funcType"] = "basic_vfunc"
|
||||
parsedFunc.funcType = "basicVirtual"
|
||||
else:
|
||||
funcInfo["funcType"] = "vfunc"
|
||||
parsedFunc.funcType = "virtual"
|
||||
|
||||
######################
|
||||
################### TO SEE HOW TO PROPERLY IMPLEMENT
|
||||
######################
|
||||
funcInfo["returnType"] = ParseClassNameString(returnType) if returnType else {}
|
||||
funcInfo["className"] = ParseClassNameString(className) if className else {}
|
||||
funcInfo["funcName"] = funcName
|
||||
returnType = Utils.ReplaceIDATypes(returnType)
|
||||
returnType = Utils.CleanType(returnType)
|
||||
|
||||
returnTypeTokens = Utils.ExtractTypeTokensFromString(returnType)
|
||||
for i in range(len(returnTypeTokens)):
|
||||
typeOfReturnType: str = "returnType"
|
||||
nameOfReturnType: str = returnTypeTokens[i]
|
||||
parsedClassOfReturnType: Optional[ParsedClass] = None
|
||||
|
||||
if len(returnTypeTokens) > 1:
|
||||
if returnTypeTokens[i] in CLASS_TYPES:
|
||||
typeOfReturnType = "classReturnType"
|
||||
parsedClassOfReturnType = ParseClassStr(f"{returnTypeTokens[i]} {returnTypeTokens[i + 1]}")
|
||||
parsedFunc.returnTypes.append(ParsedParam(type=typeOfReturnType, name=nameOfReturnType, parsedClassParam=parsedClassOfReturnType))
|
||||
|
||||
#funcInfo.class = ParseClassNameString(className) if className else {}
|
||||
parsedFunc.funcName = funcName
|
||||
|
||||
# Handle special case for _purecall
|
||||
elif onlyVirtualFuncs and funcStr == "_purecall":
|
||||
funcInfo["type"] = "stripped_vfunc"
|
||||
funcInfo["returnType"] = ParseClassNameString("virtual void")
|
||||
funcInfo["funcName"] = "_StrippedVFunc1" # Simplified counter
|
||||
virtualFuncPlaceholderCounter[funcStr] += 1
|
||||
parsedFunc.funcType = "strippedVirtual"
|
||||
parsedFunc.returnTypes = [ParsedParam(type="returnType", name="virtual"), ParsedParam(type="returnType", name="void")]
|
||||
parsedFunc.funcName = f"_StrippedVFunc{virtualFuncPlaceholderCounter[funcStr]}"
|
||||
|
||||
return funcInfo
|
||||
return parsedFunc
|
||||
|
||||
def ExtractParentNamespacesAndClasses(namespacesAndClasses: list[str]) -> tuple[list[str], list[str]]:
|
||||
global parsedClassesDict
|
||||
|
||||
parentNamespaces: list[str] = []
|
||||
parentClasses: list[str] = []
|
||||
|
||||
continueOnlyWithClasses: bool = False
|
||||
for part in namespacesAndClasses:
|
||||
namespacesAndClass = "::".join(parentNamespaces + [part])
|
||||
if (namespacesAndClass not in parsedClassesDict or parsedClassesDict[namespacesAndClass].type == "namespace") and not continueOnlyWithClasses:
|
||||
parentNamespaces.append(part)
|
||||
else:
|
||||
if not continueOnlyWithClasses:
|
||||
continueOnlyWithClasses = True
|
||||
parentClasses.append(part)
|
||||
|
||||
return parentNamespaces, parentClasses
|
||||
|
||||
def ExtractAllClassSigsFromFuncSig(funcSig: str) -> list[str]:
|
||||
parts = Utils.ExtractTypeTokensFromString(funcSig)
|
||||
if not len(parts) > 1:
|
||||
return []
|
||||
|
||||
listOfClassSigs: list[str] = []
|
||||
for i in range(len(parts) - 1):
|
||||
(classType, className) = (parts[i], parts[i + 1])
|
||||
if classType in CLASS_TYPES and className:
|
||||
className = Utils.CleanEndOfClassStr(className)
|
||||
listOfClassSigs.append(f"{classType} {className}")
|
||||
return listOfClassSigs
|
||||
|
||||
def ExtractMainClassSigFromFuncSig(funcSig: str) -> str:
|
||||
for keyword in ("public:", "protected:", "private:"):
|
||||
if funcSig.startswith(keyword):
|
||||
funcSig = funcSig[len(keyword):].strip()
|
||||
break
|
||||
|
||||
paramsOpenParenIndex = funcSig.find('(')
|
||||
paramsCloseParenIndex = funcSig.rfind(')')
|
||||
if paramsOpenParenIndex == -1 or paramsCloseParenIndex == -1:
|
||||
return ""
|
||||
|
||||
remainingInputBeforeParamsParen = funcSig[:paramsOpenParenIndex].strip()
|
||||
|
||||
# Find the last space outside of angle brackets
|
||||
lastSpaceIndex = Utils.FindLastSpaceOutsideTemplates(remainingInputBeforeParamsParen)
|
||||
if lastSpaceIndex != -1:
|
||||
# Split at the last space outside angle brackets
|
||||
returnType = remainingInputBeforeParamsParen[:lastSpaceIndex].strip()
|
||||
parts = Utils.ExtractTypeTokensFromString(returnType)
|
||||
if not parts:
|
||||
return ""
|
||||
if len(parts) > 1:
|
||||
for i in range(len(parts)):
|
||||
classType = parts[i]
|
||||
if classType in CLASS_TYPES:
|
||||
return ""
|
||||
|
||||
classAndFuncName = remainingInputBeforeParamsParen[lastSpaceIndex + 1:].strip()
|
||||
|
||||
# Find the last class separator outside of angle brackets
|
||||
lastClassSeparatorIndex = Utils.FindLastClassSeparatorOutsideTemplates(classAndFuncName)
|
||||
|
||||
if lastClassSeparatorIndex != -1:
|
||||
namespacesAndClasses = classAndFuncName[:lastClassSeparatorIndex]
|
||||
funcName = classAndFuncName[lastClassSeparatorIndex+2:]
|
||||
else:
|
||||
classParts = Utils.SplitByClassSeparatorOutsideTemplates(classAndFuncName)
|
||||
namespacesAndClasses = "::".join(classParts[:-1]) if len(classParts) > 1 else ""
|
||||
funcName = classParts[-1]
|
||||
else:
|
||||
# No space found, try to find class separator
|
||||
lastClassSeparatorIndex = Utils.FindLastClassSeparatorOutsideTemplates(remainingInputBeforeParamsParen)
|
||||
|
||||
if lastClassSeparatorIndex != -1:
|
||||
classAndFuncName = remainingInputBeforeParamsParen
|
||||
namespacesAndClasses = classAndFuncName[:lastClassSeparatorIndex]
|
||||
funcName = classAndFuncName[lastClassSeparatorIndex+2:]
|
||||
else:
|
||||
funcName = remainingInputBeforeParamsParen
|
||||
namespacesAndClasses = ""
|
||||
|
||||
return f"{'class' if namespacesAndClasses.endswith(funcName) else 'namespace'} {namespacesAndClasses}" if namespacesAndClasses else ""
|
||||
|
||||
def ParseAllClasses():
|
||||
global parsedClassesDict
|
||||
parsedClassesDict = {}
|
||||
|
||||
# Get and parse all classes that are mentioned in a func sig, such as "class cbs::CPointer" in the params here: 'bool cbs::IsInDynamicRoot(class cbs::CPointer<class cbs::CEntity>, bool)'
|
||||
demangledExportedSigs = IDAUtils.GetDemangledExportedSigs()
|
||||
for demangledFuncSig in demangledExportedSigs:
|
||||
listOfExtractedClassSigs = ExtractAllClassSigsFromFuncSig(demangledFuncSig)
|
||||
for clsSig in listOfExtractedClassSigs:
|
||||
parsedClass = ParseClassStr(clsSig)
|
||||
if not parsedClass:
|
||||
continue
|
||||
|
||||
alreadyParsedClass = parsedClassesDict.get(parsedClass.fullClassName)
|
||||
if not alreadyParsedClass:
|
||||
parsedClassesDict[parsedClass.fullClassName] = parsedClass
|
||||
elif parsedClass.templateParams and parsedClass.templateParams[0] not in alreadyParsedClass.templateParams:
|
||||
alreadyParsedClass.templateParams.extend(parsedClass.templateParams)
|
||||
|
||||
# Get and parse the main class that is mentioned in a func sig, such as "cbs" from "cbs::IsInDynamicRoot" in the name of the function here: 'bool cbs::IsInDynamicRoot(class cbs::CPointer<class cbs::CEntity>, bool)'
|
||||
for demangledFuncSig in demangledExportedSigs:
|
||||
extractedMainClassSig = ExtractMainClassSigFromFuncSig(demangledFuncSig)
|
||||
parsedClass = ParseClassStr(extractedMainClassSig)
|
||||
if not parsedClass:
|
||||
continue
|
||||
|
||||
if parsedClass.fullClassName not in parsedClassesDict:
|
||||
parsedClassesDict[parsedClass.fullClassName] = parsedClass
|
||||
elif parsedClass.type == "class" and parsedClassesDict[parsedClass.fullClassName].type == "namespace":
|
||||
parsedClassesDict[parsedClass.fullClassName].type = "class"
|
||||
|
||||
# Fix parsed classes by setting the right parent namespaces and classes (because cbs might be a parent class and not a parent namespace, which will later change how the header generates for the class)
|
||||
for parsedClass in parsedClassesDict.values():
|
||||
parentNamespaces, parentClasses = ExtractParentNamespacesAndClasses(parsedClass.parentNamespaces)
|
||||
parsedClass.parentNamespaces = parentNamespaces
|
||||
parsedClass.parentClasses = parentClasses
|
||||
if (parentClasses or parsedClass.templateParams) and parsedClass.type == "namespace":
|
||||
parsedClass.type = "class"
|
||||
|
||||
# Find and move child classes to parent classes
|
||||
for parsedClass in list(parsedClassesDict.values()):
|
||||
if not parsedClass.parentNamespaces and not parsedClass.parentClasses:
|
||||
continue
|
||||
|
||||
parentName = ""
|
||||
if parsedClass.parentNamespaces:
|
||||
parentName = parsedClass.parentNamespaces[-1]
|
||||
elif parsedClass.parentClasses:
|
||||
parentName = parsedClass.parentClasses[-1]
|
||||
if not parentName:
|
||||
continue
|
||||
|
||||
parentClass = None
|
||||
if parsedClass.parentNamespaces:
|
||||
parentClass = next((parentClass for parentClass in parsedClassesDict.values() if parentClass.name == parentName and parentClass.parentNamespaces == parsedClass.parentNamespaces[:-1]), None)
|
||||
elif parsedClass.parentClasses:
|
||||
parentClass = next((parentClass for parentClass in parsedClassesDict.values() if parentClass.name == parentName and parentClass.parentClasses == parsedClass.parentClasses[:-1]), None)
|
||||
if not parentClass:
|
||||
continue
|
||||
|
||||
parentClass.childClasses.append(parsedClass)
|
||||
del parsedClassesDict[parsedClass.fullClassName]
|
||||
|
||||
def GetAllParsedClasses():
|
||||
ParseAllClasses()
|
||||
print(json.dumps(parsedClassesDict, indent=4))
|
@ -7,12 +7,14 @@ def Main():
|
||||
UI.OpenMainDlg()
|
||||
|
||||
# Reload modules to apply any changes
|
||||
from ExportClassH import Utils, Config, ClassDefs, RTTIAnalyzer, ClassParser, HeaderGen, ProjectManager
|
||||
from ExportClassH import Config, Utils, IDAUtils, ClassDefs, JSONGen#, RTTIAnalyzer, ClassParser, HeaderGen, ProjectManager
|
||||
importlib.reload(Config)
|
||||
importlib.reload(Utils)
|
||||
importlib.reload(IDAUtils)
|
||||
importlib.reload(ClassDefs)
|
||||
importlib.reload(RTTIAnalyzer)
|
||||
importlib.reload(ClassParser)
|
||||
importlib.reload(HeaderGen)
|
||||
importlib.reload(ProjectManager)
|
||||
importlib.reload(JSONGen)
|
||||
# importlib.reload(RTTIAnalyzer)
|
||||
# importlib.reload(ClassParser)
|
||||
# importlib.reload(HeaderGen)
|
||||
# importlib.reload(ProjectManager)
|
||||
importlib.reload(UI)
|
@ -6,9 +6,9 @@ import ida_ida
|
||||
import ida_hexrays
|
||||
|
||||
from ExportClassH import Utils
|
||||
from ExportClassH.ClassDefs import ClassName
|
||||
from ExportClassH.ClassDefs import ParsedClass
|
||||
|
||||
def GetVTablePtr(targetClass: ClassName, targetClassRTTIName: str = "") -> int:
|
||||
def GetVTablePtr(targetClass: ParsedClass, targetClassRTTIName: str = "") -> int:
|
||||
"""
|
||||
Find vtable pointer for a class using RTTI information.
|
||||
Supports both simple class names and namespaced class names.
|
||||
@ -21,7 +21,7 @@ def GetVTablePtr(targetClass: ClassName, targetClassRTTIName: str = "") -> int:
|
||||
# Use provided RTTI name if available (for templates), otherwise generate it
|
||||
if not targetClassRTTIName:
|
||||
# Check if this is a templated class
|
||||
typeDescriptorName: str = Utils.GetMangledTypePrefix(targetClass.namespaces, targetClass.name)
|
||||
typeDescriptorName: str = Utils.GetMangledTypePrefix(targetClass.parentNamespaces + targetClass.parentClasses, targetClass.name)
|
||||
else:
|
||||
# Use the provided RTTI name directly
|
||||
typeDescriptorName: str = targetClassRTTIName
|
||||
@ -43,7 +43,7 @@ def GetVTablePtr(targetClass: ClassName, targetClassRTTIName: str = "") -> int:
|
||||
|
||||
typeDescriptorPatternAddr: int = ida_bytes.bin_search(rdataStartAddr, ida_ida.cvar.inf.max_ea, compiledIDAPattern, ida_bytes.BIN_SEARCH_FORWARD)
|
||||
if typeDescriptorPatternAddr == idc.BADADDR:
|
||||
print(f"Type descriptor pattern '{typeDescriptorName}' not found for {targetClass.namespacedClassedName}.")
|
||||
print(f"Type descriptor pattern '{typeDescriptorName}' not found for {targetClass.fullClassStr}.")
|
||||
return 0
|
||||
|
||||
# Adjust to get RTTI type descriptor
|
||||
@ -89,17 +89,17 @@ def GetVTablePtr(targetClass: ClassName, targetClassRTTIName: str = "") -> int:
|
||||
|
||||
return vtableAddr
|
||||
|
||||
print(f"Failed to locate vtable pointer for {targetClass.namespacedClassedName}.")
|
||||
print(f"Failed to locate vtable pointer for {targetClass.fullClassStr}.")
|
||||
return 0
|
||||
|
||||
def GetDemangledVTableFuncSigs(targetClass: ClassName, targetClassRTTIName: str = "") -> list[tuple[str, str]]:
|
||||
def GetDemangledVTableFuncSigs(targetClass: ParsedClass, targetClassRTTIName: str = "") -> list[tuple[str, str]]:
|
||||
"""
|
||||
Get the ordered list of function names from a class's vtable.
|
||||
For templated classes, you can provide the rtti_name pattern.
|
||||
"""
|
||||
vtablePtr: int = GetVTablePtr(targetClass, targetClassRTTIName)
|
||||
if not vtablePtr:
|
||||
print(f"Vtable pointer not found for {targetClass.namespacedClassedName}.")
|
||||
print(f"Vtable pointer not found for {targetClass.fullClassStr}.")
|
||||
return []
|
||||
|
||||
demangledVTableFuncSigsList: list[tuple[str, str]] = []
|
||||
|
@ -2,8 +2,7 @@ import os
|
||||
import json
|
||||
import ida_kernwin
|
||||
|
||||
from ExportClassH import Config, HeaderGen, ProjectManager
|
||||
from ExportClassH.ClassDefs import ClassName
|
||||
from ExportClassH import Config, JSONGen
|
||||
|
||||
def SetConfigVars(settings):
|
||||
Config.PROJECT_INCLUDES_PATH = settings["PROJECT_INCLUDES_PATH"]
|
||||
@ -118,14 +117,15 @@ def OpenMainDlg():
|
||||
|
||||
if selectedOption == 0:
|
||||
print("[INFO] Update Project Code selected!")
|
||||
ProjectManager.ProcessExistingHeaders()
|
||||
#ProjectManager.ProcessExistingHeaders()
|
||||
elif selectedOption == 1:
|
||||
print("[INFO] Generate Class Code selected!")
|
||||
targetClassName = ida_kernwin.ask_str("", 0, "Enter target class name:")
|
||||
if not targetClassName:
|
||||
print("No target class specified. Aborting.")
|
||||
return
|
||||
HeaderGen.ExportClassHeader(ClassName(targetClassName))
|
||||
# targetClassName = ida_kernwin.ask_str("", 0, "Enter target class name:")
|
||||
# if not targetClassName:
|
||||
# print("No target class specified. Aborting.")
|
||||
# return
|
||||
#HeaderGen.ExportClassHeader(ClassName(targetClassName))
|
||||
JSONGen.GetAllParsedClasses()
|
||||
elif selectedOption == 2:
|
||||
print("[INFO] Settings selected!")
|
||||
OpenSettingsDlg() # Open settings when selected
|
||||
|
@ -1,4 +1,5 @@
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import Tuple
|
||||
import ida_nalt
|
||||
import ida_bytes
|
||||
@ -11,7 +12,7 @@ IDA_NALT_ENCODING = ida_nalt.get_default_encoding_idx(ida_nalt.BPU_1B)
|
||||
def FixTypeSpacing(type: str) -> str:
|
||||
"""Fix spacing for pointers/references, commas, and angle brackets."""
|
||||
type = re.sub(r'\s+([*&])', r'\1', type) # Remove space before '*' or '&'
|
||||
type = re.sub(r'([*&])(?![\s*&])', r'\1 ', type) # Ensure '*' or '&' is followed by one space if it's not already.
|
||||
type = re.sub(r'([*&])(?![\s*&])', r'\1 ', type) # Ensure '*' or '&' is followed by one space if it's not already.
|
||||
type = re.sub(r'\s*,\s*', ', ', type) # Ensure comma followed by one space
|
||||
type = re.sub(r'<\s+', '<', type) # Remove space after '<'
|
||||
type = re.sub(r'\s+>', '>', type) # Remove space before '>'
|
||||
@ -19,6 +20,16 @@ def FixTypeSpacing(type: str) -> str:
|
||||
type = re.sub(r'\s+', ' ', type) # Collapse multiple spaces
|
||||
return type.strip()
|
||||
|
||||
def CleanDoubleSpaces(str: str) -> str:
|
||||
return " ".join(str.split())
|
||||
|
||||
def CleanEndOfClassStr(clsStr: str) -> str:
|
||||
clsStr = clsStr.removesuffix("const")
|
||||
while clsStr and clsStr[-1] in {')', ',', '&', '*'}:
|
||||
clsStr = clsStr[:-1]
|
||||
clsStr = clsStr.removesuffix("const")
|
||||
return clsStr
|
||||
|
||||
def CleanType(type: str) -> str:
|
||||
"""Remove unwanted tokens from a type string, then fix spacing."""
|
||||
type = re.sub(r'\b(__cdecl|__fastcall|__ptr64)\b', '', type)
|
||||
@ -28,6 +39,7 @@ def ReplaceIDATypes(type: str) -> str:
|
||||
"""Replace IDA types with normal ones"""
|
||||
return type.replace("unsigned __int64", "uint64_t").replace("_QWORD", "uint64_t").replace("__int64", "int64_t").replace("unsigned int", "uint32_t")
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def ExtractTypeTokensFromString(types: str) -> list[str]:
|
||||
"""Extract potential type names from a string, properly handling template types."""
|
||||
if not types:
|
||||
@ -60,30 +72,65 @@ def ExtractTypeTokensFromString(types: str) -> list[str]:
|
||||
# Filter out empty strings
|
||||
return [word.strip() for word in result if word]
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def SplitByCommaOutsideTemplates(params: str) -> list[str]:
|
||||
parts = []
|
||||
current = []
|
||||
depth = 0
|
||||
i = 0
|
||||
|
||||
for char in params:
|
||||
if char == '<':
|
||||
while i < len(params):
|
||||
if params[i] == '<':
|
||||
depth += 1
|
||||
elif char == '>':
|
||||
elif params[i] == '>':
|
||||
# It's good to check for consistency:
|
||||
if depth > 0:
|
||||
depth -= 1
|
||||
# If we see a comma at top level, split here.
|
||||
if char == ',' and depth == 0:
|
||||
|
||||
# If we see a , at top level, split here.
|
||||
if params[i] == ',' and depth == 0:
|
||||
parts.append(''.join(current).strip())
|
||||
current = []
|
||||
i += 1
|
||||
else:
|
||||
current.append(char)
|
||||
current.append(params[i])
|
||||
i += 1
|
||||
|
||||
# Append any remaining characters as the last parameter.
|
||||
if current:
|
||||
parts.append(''.join(current).strip())
|
||||
return parts
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def SplitByClassSeparatorOutsideTemplates(params: str) -> list[str]:
|
||||
parts = []
|
||||
current = []
|
||||
depth = 0
|
||||
i = 0
|
||||
|
||||
while i < len(params):
|
||||
if params[i] == '<':
|
||||
depth += 1
|
||||
elif params[i] == '>':
|
||||
# It's good to check for consistency:
|
||||
if depth > 0:
|
||||
depth -= 1
|
||||
|
||||
# If we see a :: at top level, split here.
|
||||
if params[i] == ':' and params[i + 1] == ":" and depth == 0:
|
||||
parts.append(''.join(current).strip())
|
||||
current = []
|
||||
i += 2
|
||||
else:
|
||||
current.append(params[i])
|
||||
i += 1
|
||||
|
||||
# Append any remaining characters as the last parameter.
|
||||
if current:
|
||||
parts.append(''.join(current).strip())
|
||||
return parts
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def FindLastSpaceOutsideTemplates(s: str) -> int:
|
||||
"""Return the index of the last space in s that is not inside '<' and '>'."""
|
||||
depth = 0
|
||||
@ -97,6 +144,7 @@ def FindLastSpaceOutsideTemplates(s: str) -> int:
|
||||
return i
|
||||
return -1
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def FindLastClassSeparatorOutsideTemplates(s: str) -> int:
|
||||
"""Return the index of the last occurrence of "::" in s that is not inside '<' and '>'."""
|
||||
depth = 0
|
||||
@ -137,6 +185,7 @@ def GetMangledTypePrefix(namespaces: tuple[str], className: str) -> str:
|
||||
# IDA pattern search utilities
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def BytesToIDAPattern(data: bytes) -> str:
|
||||
"""Convert bytes to IDA-friendly hex pattern string."""
|
||||
return " ".join("{:02X}".format(b) for b in data)
|
||||
|
Reference in New Issue
Block a user