mirror of
https://github.com/EricPlayZ/EGameTools.git
synced 2025-07-18 17:37:53 +08:00
script changes
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
_IDAScripts/.env
|
||||
_IDAScripts/_generated/
|
||||
_IDAScripts/generated/*.json
|
||||
|
||||
# Ultimate-ASI-Loader
|
||||
data/
|
||||
@ -408,3 +408,4 @@ FodyWeavers.xsd
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
/_IDAScripts/generated/parsed-classes.json
|
||||
|
@ -157,7 +157,7 @@
|
||||
</OptimizeReferences>
|
||||
<EnableCOMDATFolding>
|
||||
</EnableCOMDATFolding>
|
||||
<AdditionalLibraryDirectories>..\EGameSDK\deps\MinHook\lib;deps\freetype\lib;..\EGameSDK\deps\game_libs;</AdditionalLibraryDirectories>
|
||||
<AdditionalLibraryDirectories>..\EGameSDK\deps\MinHook\lib;deps\freetype\lib;..\EGameSDK\deps\game_libs;$(SolutionDir)$(Platform)\$(Configuration)\;</AdditionalLibraryDirectories>
|
||||
<AssemblyDebug>true</AssemblyDebug>
|
||||
<ImportLibrary />
|
||||
<FixedBaseAddress>
|
||||
@ -192,7 +192,7 @@
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>DebugFull</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalLibraryDirectories>..\EGameSDK\deps\MinHook\lib;deps\freetype\lib;..\EGameSDK\deps\game_libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalLibraryDirectories>..\EGameSDK\deps\MinHook\lib;deps\freetype\lib;..\EGameSDK\deps\game_libs;$(SolutionDir)$(Platform)\$(Configuration)\;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>DbgHelp.lib;Version.lib;EGameSDK.lib;libMinHook-x64-v141-mdd.lib;engine_x64_rwdi.lib;freetype-md.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<ImportLibrary />
|
||||
<AdditionalOptions>/NOIMPLIB /NOEXP %(AdditionalOptions)</AdditionalOptions>
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"PROJECT_INCLUDES_PATH": "D:\\PROJECTS\\Visual Studio\\EGameSDK\\EGameSDK\\include",
|
||||
"OUTPUT_PATH": "D:\\PROJECTS\\Visual Studio\\EGameSDK\\_IDAScripts",
|
||||
"LAST_CLICKED_RADIO": 1
|
||||
"LAST_CLICKED_RADIO": 2
|
||||
}
|
@ -1,63 +1,54 @@
|
||||
from typing import Optional, List, Dict, get_type_hints
|
||||
from prodict import Prodict
|
||||
|
||||
class ParsedParam(Prodict):
|
||||
type: str
|
||||
name: str
|
||||
parsedClassParam: Optional["ParsedClass"]
|
||||
from __future__ import annotations
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, SkipValidation
|
||||
|
||||
def init(self):
|
||||
self.type = ""
|
||||
self.name = ""
|
||||
self.parsedClassParam = None
|
||||
def DefaultPydanticSerializer(obj):
|
||||
if hasattr(obj, "model_dump"):
|
||||
return obj.model_dump(exclude_none=True)
|
||||
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
|
||||
|
||||
class ParsedClass(Prodict):
|
||||
type: str
|
||||
parentNamespaces: List[str]
|
||||
parentClasses: List[str]
|
||||
name: str
|
||||
templateParams: List["ParsedParam"]
|
||||
fullClassName: str
|
||||
childClasses: Prodict
|
||||
functions: List["ParsedFunction"]
|
||||
class ParsedParam(BaseModel):
|
||||
type: str = ""
|
||||
name: str = ""
|
||||
parsedClassParam: Optional[SkipValidation[ParsedClass]] = None
|
||||
|
||||
def init(self):
|
||||
self.type = ""
|
||||
self.parentNamespaces = []
|
||||
self.parentClasses = []
|
||||
self.name = ""
|
||||
self.templateParams = []
|
||||
self.fullClassName = ""
|
||||
self.childClasses = Prodict()
|
||||
self.functions = []
|
||||
class ParsedClass(BaseModel):
|
||||
type: str = ""
|
||||
parentNamespaces: List[str] = []
|
||||
parentClasses: List[str] = []
|
||||
classDependencies: List[str] = []
|
||||
name: str = ""
|
||||
templateParams: List[ParsedParam] = []
|
||||
fullClassName: str = ""
|
||||
childClasses: dict = {}
|
||||
classVars: List[ParsedClassVar] = []
|
||||
virtualFunctions: List[ParsedFunction] = []
|
||||
functions: List[ParsedFunction] = []
|
||||
|
||||
class ParsedFunction(Prodict):
|
||||
type: str
|
||||
funcType: str
|
||||
access: str
|
||||
returnTypes: List[ParsedParam]
|
||||
parentNamespaces: List[str]
|
||||
parentClasses: List[str]
|
||||
fullClassName: str
|
||||
funcName: str
|
||||
params: List[ParsedParam]
|
||||
const: bool
|
||||
fullFuncSig: str
|
||||
class ParsedFunction(BaseModel):
|
||||
type: str = "function"
|
||||
funcType: str = "function"
|
||||
access: str = "public"
|
||||
returnTypes: List[ParsedParam] = []
|
||||
parentNamespaces: List[str] = []
|
||||
parentClasses: List[str] = []
|
||||
fullClassName: str = ""
|
||||
funcName: str = ""
|
||||
params: List[ParsedParam] = []
|
||||
const: bool = False
|
||||
fullFuncSig: str = ""
|
||||
|
||||
def init(self):
|
||||
self.type = "function"
|
||||
self.funcType = "function"
|
||||
self.access = "public"
|
||||
self.returnTypes = []
|
||||
self.parentNamespaces = []
|
||||
self.parentClasses = []
|
||||
self.fullClassName = ""
|
||||
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)
|
||||
class ParsedClassVar(BaseModel):
|
||||
type: str = "classVar"
|
||||
access: str = "public"
|
||||
varTypes: List[ParsedParam] = []
|
||||
parentNamespaces: List[str] = []
|
||||
parentClasses: List[str] = []
|
||||
fullClassName: str = ""
|
||||
varName: str = ""
|
||||
fullClassVarSig: str = ""
|
||||
|
||||
ParsedParam.model_rebuild()
|
||||
ParsedClass.model_rebuild()
|
||||
ParsedFunction.model_rebuild()
|
||||
ParsedClassVar.model_rebuild()
|
@ -5,15 +5,13 @@ INTERNAL_SCRIPT_NAME = "ExportClassH"
|
||||
PROJECT_INCLUDES_PATH = r"D:\PROJECTS\Visual Studio\EGameSDK\EGameSDK\include"
|
||||
OUTPUT_PATH = r"D:\PROJECTS\Visual Studio\EGameSDK\_IDAScripts"
|
||||
HEADER_OUTPUT_PATH = os.path.join(OUTPUT_PATH, "generated")
|
||||
CACHE_OUTPUT_PATH = os.path.join(OUTPUT_PATH, "cache")
|
||||
PARSED_VARS_CACHE_FILENAME = os.path.join(CACHE_OUTPUT_PATH, "parsedClassVarsByClass.cache")
|
||||
PARSED_FUNCS_CACHE_FILENAME = os.path.join(CACHE_OUTPUT_PATH, "parsedFuncsByClass.cache")
|
||||
INPUT_MD5 = bytes()
|
||||
LAST_CLICKED_RADIO = 0
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"PROJECT_INCLUDES_PATH": PROJECT_INCLUDES_PATH,
|
||||
"OUTPUT_PATH": OUTPUT_PATH,
|
||||
"LAST_CLICKED_RADIO": LAST_CLICKED_RADIO,
|
||||
"LAST_CLICKED_RADIO": LAST_CLICKED_RADIO
|
||||
}
|
||||
CONFIG_FILE = os.path.join(os.path.join(os.path.dirname(__file__), os.pardir), "ExportClassH.json")
|
||||
PARSED_CLASSES_OUTPUT_FILE = os.path.join(HEADER_OUTPUT_PATH, "parsed-classes.json")
|
@ -1,40 +1,39 @@
|
||||
import os
|
||||
|
||||
from ExportClassH import ClassGen, Utils, Config
|
||||
from ExportClassH.ClassDefs import ClassName, ParsedFunction, ParsedClassVar
|
||||
|
||||
processedClasses: set[str] = set()
|
||||
from ExportClassH import JSONGen, Utils, Config
|
||||
from ExportClassH.ClassDefs import ParsedClass, ParsedFunction, ParsedClassVar
|
||||
|
||||
currentAccess: str = "public"
|
||||
def GenerateClassVarCode(classVar: ParsedClassVar) -> list[str]:
|
||||
def GenerateClassVarCode(classVar: ParsedClassVar, indentLevel: str) -> list[str]:
|
||||
"""Generate code for a single class variable."""
|
||||
global currentAccess
|
||||
|
||||
access: str = f"{classVar.access}:" if classVar.access else ""
|
||||
if currentAccess == classVar.access:
|
||||
access = "\t"
|
||||
access = ""
|
||||
else:
|
||||
currentAccess = classVar.access
|
||||
|
||||
if classVar.varType:
|
||||
varType: str = Utils.ReplaceIDATypes(classVar.varType.fullName)
|
||||
varType = Utils.CleanType(varType)
|
||||
if varType:
|
||||
varType += " "
|
||||
varType = "GAME_IMPORT " + varType
|
||||
if classVar.varTypes:
|
||||
varTypesList: list[str] = [varType.name for varType in classVar.varTypes if varType]
|
||||
varTypes: str = Utils.ReplaceIDATypes(" ".join(varTypesList))
|
||||
varTypes = Utils.CleanType(varTypes)
|
||||
if varTypes:
|
||||
varTypes += " "
|
||||
varTypes = "GAME_IMPORT " + varTypes
|
||||
else:
|
||||
varType: str = ""
|
||||
varTypes: str = ""
|
||||
|
||||
classVarSig: str = f"{varType}{classVar.varName}"
|
||||
classVarSig: str = f"{varTypes}{classVar.varName}"
|
||||
|
||||
classVarLines: list[str] = []
|
||||
if access:
|
||||
classVarLines.append(access)
|
||||
classVarLines.append(f"{indentLevel}{access}")
|
||||
if classVarSig:
|
||||
classVarLines.append(f"\t{classVarSig};")
|
||||
classVarLines.append(f"{indentLevel}\t{classVarSig};")
|
||||
return classVarLines
|
||||
|
||||
def GenerateClassFuncCode(func: ParsedFunction, vtFuncIndex: int = 0) -> list[str]:
|
||||
def GenerateClassFuncCode(func: ParsedFunction, indentLevel: str, vtFuncIndex: int = 0) -> list[str]:
|
||||
"""Generate code for a single class method."""
|
||||
global currentAccess
|
||||
|
||||
@ -45,25 +44,24 @@ def GenerateClassFuncCode(func: ParsedFunction, vtFuncIndex: int = 0) -> list[st
|
||||
currentAccess = func.access
|
||||
|
||||
const: str = " const" if func.const else ""
|
||||
stripped_vfunc: str = " = 0" if func.type == "stripped_vfunc" else ""
|
||||
strippedVirtual: str = " = 0" if func.type == "strippedVirtual" else ""
|
||||
|
||||
if func.returnType:
|
||||
returnType: str = Utils.ReplaceIDATypes(func.returnType.fullName)
|
||||
if func.returnTypes:
|
||||
returnTypesList: list[str] = [returnType.name for returnType in func.returnTypes if returnType]
|
||||
returnType: str = Utils.ReplaceIDATypes(" ".join(returnTypesList))
|
||||
returnType = Utils.CleanType(returnType)
|
||||
if returnType:
|
||||
if func.type == "basic_vfunc":
|
||||
if func.type == "basicVirtual":
|
||||
returnType = returnType.removeprefix("virtual").strip()
|
||||
else:
|
||||
returnType += " "
|
||||
else:
|
||||
returnType: str = ""
|
||||
if func.type != "stripped_vfunc" and func.type != "basic_vfunc":
|
||||
if func.type != "strippedVirtual" and func.type != "basicVirtual":
|
||||
returnType = "GAME_IMPORT " + returnType
|
||||
|
||||
if func.params:
|
||||
paramsList: list[str] = []
|
||||
for param in func.params:
|
||||
paramsList.append(param.fullName)
|
||||
paramsList: list[str] = [param.name for param in func.params if param]
|
||||
params: str = Utils.ReplaceIDATypes(", ".join(paramsList))
|
||||
params = Utils.CleanType(params)
|
||||
if params == "void":
|
||||
@ -72,143 +70,129 @@ def GenerateClassFuncCode(func: ParsedFunction, vtFuncIndex: int = 0) -> list[st
|
||||
params: str = ""
|
||||
|
||||
targetParams: str = ""
|
||||
if func.type == "basic_vfunc":
|
||||
targetParams = ClassGen.ExtractParamNames(params)
|
||||
if func.type == "basicVirtual":
|
||||
targetParams = Utils.ExtractParamNames(params)
|
||||
targetParams = ", " + targetParams if targetParams else ""
|
||||
|
||||
funcSig: str = f"{returnType}{func.funcName}({params}){const}{stripped_vfunc}" if func.type != "basic_vfunc" else f"VIRTUAL_CALL({vtFuncIndex}, {returnType}, {func.funcName}, ({params}){targetParams})"
|
||||
funcSig: str = f"{returnType}{func.funcName}({params}){const}{strippedVirtual}" if func.type != "basicVirtual" else f"VIRTUAL_CALL({vtFuncIndex}, {returnType}, {func.funcName}, ({params}){targetParams})"
|
||||
|
||||
classFuncLines: list[str] = []
|
||||
if access:
|
||||
classFuncLines.append(access)
|
||||
classFuncLines.append(f"{indentLevel}{access}")
|
||||
if funcSig:
|
||||
classFuncLines.append(f"\t{funcSig};")
|
||||
classFuncLines.append(f"{indentLevel}\t{funcSig};")
|
||||
return classFuncLines
|
||||
|
||||
def GenerateClassContent(allParsedElements: tuple[list[ParsedClassVar], list[ParsedFunction], list[ParsedFunction]]) -> list[str]:
|
||||
indentLevel = ""
|
||||
def GenerateClassContent(targetClass: ParsedClass, indentLevel: str) -> list[str]:
|
||||
"""
|
||||
Generate the content to be inserted into an existing header file.
|
||||
This is just the class members, not the full header with includes, etc.
|
||||
"""
|
||||
global currentAccess
|
||||
|
||||
parsedVars, parsedVtFuncs, parsedFuncs = allParsedElements
|
||||
if not parsedVars and not parsedVtFuncs and not parsedFuncs:
|
||||
return []
|
||||
|
||||
firstVarOrFuncAccess = ""
|
||||
currentAccess = ""
|
||||
|
||||
# Generate class content (just the members)
|
||||
contentLines = [f"#pragma region GENERATED by ExportClassToCPPH.py"]
|
||||
contentLines = []
|
||||
|
||||
# Add class variables
|
||||
for classVar in parsedVars:
|
||||
for parsedClassVar in targetClass.classVars:
|
||||
if not firstVarOrFuncAccess:
|
||||
firstVarOrFuncAccess = classVar.access
|
||||
contentLines.extend(GenerateClassVarCode(classVar))
|
||||
firstVarOrFuncAccess = parsedClassVar.access
|
||||
contentLines.extend(GenerateClassVarCode(parsedClassVar, indentLevel))
|
||||
|
||||
# Add newline between sections if both exist
|
||||
if parsedVars and (parsedVtFuncs or parsedFuncs):
|
||||
if targetClass.classVars and (targetClass.virtualFunctions or targetClass.functions):
|
||||
contentLines.append("")
|
||||
|
||||
# Add vtable functions
|
||||
for index, vTableFunc in enumerate(parsedVtFuncs):
|
||||
for index, vTableFunc in enumerate(targetClass.virtualFunctions):
|
||||
if not firstVarOrFuncAccess:
|
||||
firstVarOrFuncAccess = vTableFunc.access
|
||||
contentLines.extend(GenerateClassFuncCode(vTableFunc, index))
|
||||
contentLines.extend(GenerateClassFuncCode(vTableFunc, indentLevel, index))
|
||||
|
||||
# Add newline between sections if both exist
|
||||
if parsedVtFuncs and parsedFuncs:
|
||||
if targetClass.virtualFunctions and targetClass.functions:
|
||||
contentLines.append("")
|
||||
|
||||
# Add regular functions
|
||||
for func in parsedFuncs:
|
||||
for func in targetClass.functions:
|
||||
if not firstVarOrFuncAccess:
|
||||
firstVarOrFuncAccess = func.access
|
||||
contentLines.extend(GenerateClassFuncCode(func))
|
||||
|
||||
contentLines.append("#pragma endregion")
|
||||
contentLines.extend(GenerateClassFuncCode(func, indentLevel))
|
||||
|
||||
hasAnyContent = bool(contentLines)
|
||||
|
||||
# Insert access specifier if needed
|
||||
if not firstVarOrFuncAccess:
|
||||
contentLines.insert(1, "public:")
|
||||
if not firstVarOrFuncAccess and targetClass.type == "class":
|
||||
contentLines.insert(1, f"{indentLevel}public:")
|
||||
|
||||
childClassDefinitions: list[str] = []
|
||||
for parsedClass in targetClass.childClasses.values():
|
||||
classDefinition = GenerateClassDefinition(parsedClass, indentLevel + "\t")
|
||||
classContent = GenerateClassContent(parsedClass, indentLevel + "\t")
|
||||
|
||||
childClassDefinitions.extend(classDefinition[:len(classDefinition) - 1] + classContent + classDefinition[len(classDefinition) - 1:])
|
||||
childClassDefinitions.append("")
|
||||
|
||||
if (hasAnyContent):
|
||||
contentLines.insert(0, "#pragma region GENERATED by ExportClassToCPPH.py")
|
||||
contentLines[1:1] = childClassDefinitions
|
||||
contentLines.append("#pragma endregion")
|
||||
else:
|
||||
contentLines[0:0] = childClassDefinitions
|
||||
return contentLines
|
||||
|
||||
def GenerateClassDefinition(targetClass: ClassName, forwardDeclare: bool = False) -> list[str]:
|
||||
def GenerateClassDefinition(targetClass: ParsedClass, indentLevel: str, forwardDeclare: bool = False) -> list[str]:
|
||||
"""Generate a class definition from a list of methods."""
|
||||
classDefLines: list[str] = [f"{targetClass.type} {targetClass.name}{' {' if not forwardDeclare else ';'}"]
|
||||
if not forwardDeclare:
|
||||
forwardDeclare = (targetClass.type == "enum" or targetClass.type == "union") or not targetClass.classVars and not targetClass.virtualFunctions and not targetClass.functions and targetClass.type != "namespace"
|
||||
|
||||
classDefLines: list[str] = [f"{indentLevel}{targetClass.type} {targetClass.name}{' {' if not forwardDeclare else ';'}"]
|
||||
if not forwardDeclare:
|
||||
if targetClass.type == "class":
|
||||
classDefLines.append("public:")
|
||||
classDefLines.append("};")
|
||||
classDefLines.append(f"{indentLevel}public:")
|
||||
classDefLines.append(f"{indentLevel}}};")
|
||||
return classDefLines
|
||||
|
||||
def GenerateHeaderCode(targetClass: ClassName) -> list[str]:
|
||||
def GenerateHeaderCode(targetClass: ParsedClass, parsedClassesDict: dict[str, ParsedClass]) -> list[str]:
|
||||
"""Generate header code for a standard class (not nested)."""
|
||||
# Reset processed classes for this generation
|
||||
global processedClasses, forwardDeclarations
|
||||
processedClasses = set()
|
||||
forwardDeclarations = set()
|
||||
global indentLevel
|
||||
fullClassDefinition: list[str] = []
|
||||
|
||||
for namespace in targetClass.parentNamespaces:
|
||||
fullClassDefinition.append(f"{indentLevel}namespace {namespace} {{")
|
||||
indentLevel += "\t"
|
||||
|
||||
if targetClass.parentClasses:
|
||||
parsedParentClass = parsedClassesDict.get(targetClass.parentClasses[0])
|
||||
if parsedParentClass:
|
||||
parentClassDefinition = GenerateClassDefinition(parsedParentClass, indentLevel)
|
||||
parentClassContent = GenerateClassContent(parsedParentClass, indentLevel)
|
||||
|
||||
fullClassDefinition.extend(parentClassDefinition[:len(parentClassDefinition) - 1] + parentClassContent + parentClassDefinition[len(parentClassDefinition) - 1:])
|
||||
|
||||
# Mark target class as processed to avoid self-dependency issues
|
||||
processedClasses.add(targetClass.namespacedClassedName)
|
||||
|
||||
# Get all parsed elements for the target class
|
||||
allParsedElements = ClassGen.GetAllParsedClassVarsAndFuncs(targetClass)
|
||||
|
||||
# Generate the target class definition
|
||||
classContent = GenerateClassContent(allParsedElements)
|
||||
classDefinition = GenerateClassDefinition(targetClass)
|
||||
classDefinition = GenerateClassDefinition(targetClass, indentLevel)
|
||||
classContent = GenerateClassContent(targetClass, indentLevel)
|
||||
fullClassDefinition.extend(classDefinition[:len(classDefinition) - 1] + classContent + classDefinition[len(classDefinition) - 1:])
|
||||
|
||||
fullClassDefinition = classDefinition
|
||||
if (classContent):
|
||||
fullClassDefinition = fullClassDefinition[:len(classDefinition) - 1] + classContent + fullClassDefinition[len(classDefinition) - 1:]
|
||||
|
||||
# Wrap target class in namespace blocks if needed
|
||||
if targetClass.namespaces:
|
||||
namespaceCode = []
|
||||
indentLevel = ""
|
||||
|
||||
# Opening namespace blocks with increasing indentation
|
||||
for namespace in targetClass.namespaces:
|
||||
namespaceCode.append(f"{indentLevel}namespace {namespace} {{")
|
||||
indentLevel += "\t"
|
||||
|
||||
for cls in targetClass.classes:
|
||||
clsType: str = ClassGen.GetClassTypeFromParsedSigs(ClassName(targetClass.namespacedClassedName), allParsedElements)
|
||||
namespaceCode.append(f"{indentLevel}{clsType if clsType else 'class'} {cls} {{")
|
||||
indentLevel += "\t"
|
||||
|
||||
indentedClassDefinition = [f"{indentLevel if '#pragma' not in line else ''}{line}" for line in fullClassDefinition]
|
||||
namespaceCode.extend(indentedClassDefinition)
|
||||
|
||||
for cls in reversed(targetClass.classes):
|
||||
indentLevel = indentLevel[:-1] # Remove one level of indentation
|
||||
namespaceCode.append(f"{indentLevel}}}")
|
||||
|
||||
for namespace in reversed(targetClass.namespaces):
|
||||
indentLevel = indentLevel[:-1] # Remove one level of indentation
|
||||
namespaceCode.append(f"{indentLevel}}}")
|
||||
|
||||
fullClassDefinition = namespaceCode
|
||||
for namespace in reversed(targetClass.parentNamespaces):
|
||||
indentLevel = indentLevel[:-1] # Remove one level of indentation
|
||||
fullClassDefinition.append(f"{indentLevel}}}")
|
||||
|
||||
# Combine all parts of the header
|
||||
headerParts = ["#pragma once", r"#include <EGSDK\Imports.h>", ""]
|
||||
|
||||
# Add the target class definition
|
||||
headerParts.extend(fullClassDefinition)
|
||||
|
||||
return headerParts
|
||||
|
||||
def WriteHeaderToFile(targetClass: ClassName, headerCode: str, fileName: str) -> bool:
|
||||
def WriteHeaderToFile(targetClass: ParsedClass, headerCode: str, fileName: str) -> bool:
|
||||
"""Write the generated header code to a file."""
|
||||
outputFolderPath: str = Config.HEADER_OUTPUT_PATH
|
||||
|
||||
if targetClass.namespaces:
|
||||
if targetClass.parentNamespaces:
|
||||
# Create folder structure for namespaces
|
||||
classFolderPath: str = os.path.join(*targetClass.namespaces)
|
||||
classFolderPath: str = os.path.join(*targetClass.parentNamespaces)
|
||||
outputFolderPath: str = os.path.join(outputFolderPath, classFolderPath)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
@ -233,26 +217,43 @@ def WriteHeaderToFile(targetClass: ClassName, headerCode: str, fileName: str) ->
|
||||
print(f"Error writing header file '{outputFilePath}': {e}")
|
||||
return False
|
||||
|
||||
def ExportClassHeader(targetClass: ClassName):
|
||||
def ExportClassHeader(targetClass: str):
|
||||
"""
|
||||
Generate and save a C++ header file for the target class.
|
||||
Handles multiple levels of nested classes and also generates dependencies.
|
||||
"""
|
||||
global processedClasses
|
||||
"""
|
||||
parsedClassesDict = JSONGen.GetAllParsedClasses()
|
||||
|
||||
# Skip if we've already processed this class
|
||||
if targetClass.namespacedClassedName in processedClasses:
|
||||
print(f"Already processed class {targetClass.namespacedClassedName}, skipping.")
|
||||
parsedClass = parsedClassesDict.get(targetClass)
|
||||
if not parsedClass:
|
||||
print(f"There is no class {targetClass} available, therefore will not generate.")
|
||||
return
|
||||
|
||||
# Add to processed set to prevent infinite recursion
|
||||
processedClasses.add(targetClass.namespacedClassedName)
|
||||
|
||||
# Generate the header code
|
||||
headerCodeLines = GenerateHeaderCode(targetClass)
|
||||
if not headerCodeLines:
|
||||
print(f"No functions were found for class {targetClass.namespacedClassedName}, therefore will not generate.")
|
||||
return
|
||||
JSONGen.MoveChildClasses(parsedClassesDict)
|
||||
headerCodeLines = GenerateHeaderCode(parsedClass, parsedClassesDict)
|
||||
headerCode: str = "\n".join(headerCodeLines)
|
||||
|
||||
WriteHeaderToFile(targetClass, headerCode, f"{targetClass.name}.h")
|
||||
WriteHeaderToFile(parsedClass, headerCode, f"{parsedClass.name}.h")
|
||||
|
||||
def ExportClassHeaders():
|
||||
global indentLevel
|
||||
|
||||
parsedClassesDict = JSONGen.GetAllParsedClasses()
|
||||
JSONGen.MoveChildClasses(parsedClassesDict)
|
||||
|
||||
for (parsedClassName, parsedClass) in parsedClassesDict.items():
|
||||
headerCodeLines = GenerateHeaderCode(parsedClass, parsedClassesDict)
|
||||
headerCode: str = "\n".join(headerCodeLines)
|
||||
|
||||
WriteHeaderToFile(parsedClass, headerCode, f"{parsedClass.name}.h" if parsedClass.type != "namespace" else "")
|
||||
# classDefinition = GenerateClassDefinition(parsedClass, indentLevel)
|
||||
# classContent = GenerateClassContent(parsedClass, indentLevel)
|
||||
|
||||
# # Combine all parts of the header
|
||||
# headerParts = ["#pragma once", r"#include <EGSDK\Imports.h>", ""]
|
||||
|
||||
# # Add the target class definition
|
||||
# headerParts.extend(classDefinition[:len(classDefinition) - 1] + classContent + classDefinition[len(classDefinition) - 1:])
|
||||
# headerCode: str = "\n".join(headerParts)
|
||||
|
||||
# WriteHeaderToFile(parsedClass, headerCode, f"{parsedClass.name}.h" if parsedClass.type != "namespace" else "")
|
@ -1,7 +1,13 @@
|
||||
from functools import cache
|
||||
import idc
|
||||
import idaapi
|
||||
import idautils
|
||||
import ida_bytes
|
||||
import ida_nalt
|
||||
|
||||
from ExportClassH import Utils
|
||||
IDA_NALT_ENCODING = ida_nalt.get_default_encoding_idx(ida_nalt.BPU_1B)
|
||||
|
||||
idaRTTIStrings: dict[bytes, dict[str, int]] = {}
|
||||
|
||||
# def GetDemangledExportedSigs() -> list[str]:
|
||||
# """
|
||||
@ -20,21 +26,107 @@ from ExportClassH import Utils
|
||||
# sigs_set.add(demangledExportedSig)
|
||||
# return list(sigs_set)
|
||||
|
||||
def GetDemangledExportedSigs() -> list[str]:
|
||||
def GetDemangledExportedSigs(inputMD5: bytes) -> 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)
|
||||
sigsSet = set()
|
||||
entryQty = idc.get_entry_qty()
|
||||
for i in range(entryQty):
|
||||
#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)
|
||||
demangledExportedSig: str = DemangleSig(exportedSig)
|
||||
if demangledExportedSig and "~" not in demangledExportedSig and not demangledExportedSig.endswith("::$TSS0") and "::`vftable'" not in demangledExportedSig:
|
||||
sigs_set.add((i + 1, demangledExportedSig))
|
||||
sigsSet.add((i + 1, demangledExportedSig))
|
||||
|
||||
return [sig for _, sig in sorted(sigs_set)]
|
||||
return [sig for _, sig in sorted(sigsSet)]
|
||||
|
||||
@cache
|
||||
def DemangleSig(sig: str) -> str:
|
||||
return idaapi.demangle_name(sig, idaapi.MNG_LONG_FORM)
|
||||
|
||||
@cache
|
||||
def GetMangledTypePrefix(namespaces: tuple[str, ...], className: str) -> str:
|
||||
"""
|
||||
Get the appropriate mangled type prefix for a class name.
|
||||
For class "X" this would be ".?AVX@@"
|
||||
For class "NS::X" this would be ".?AVX@NS@@"
|
||||
For templated classes, best to use get_mangled_name_for_template instead.
|
||||
"""
|
||||
if not namespaces:
|
||||
return f".?AV{className}@@"
|
||||
|
||||
# For namespaced classes, the format is .?AVClassName@Namespace@@
|
||||
# For nested namespaces, they are separated with @ in reverse order
|
||||
mangledNamespaces = "@".join(reversed(namespaces))
|
||||
return f".?AV{className}@{mangledNamespaces}@@"
|
||||
|
||||
@cache
|
||||
def BytesToIDAPattern(data: bytes) -> str:
|
||||
"""Convert bytes to IDA-friendly hex pattern string."""
|
||||
return " ".join("{:02X}".format(b) for b in data)
|
||||
|
||||
@cache
|
||||
def GetSectionInfo(inputMD5: bytes, sectionName: str) -> tuple[int, int]:
|
||||
"""Get start address and size of a specified section."""
|
||||
for seg_ea in idautils.Segments():
|
||||
if idc.get_segm_name(seg_ea) == sectionName:
|
||||
start = seg_ea
|
||||
end = idc.get_segm_end(seg_ea)
|
||||
return start, end - start
|
||||
return 0, 0
|
||||
|
||||
@cache
|
||||
def GetIDARTTIStringsList(inputMD5: bytes) -> dict[str, int]:
|
||||
global idaRTTIStrings
|
||||
|
||||
rttiStrings = idaRTTIStrings.get(inputMD5)
|
||||
if not rttiStrings:
|
||||
idaRTTIStrings[inputMD5] = {}
|
||||
strings = idautils.Strings()
|
||||
for stringItem in strings:
|
||||
if not stringItem:
|
||||
continue
|
||||
s = str(stringItem)
|
||||
if s.startswith(".?AV"):
|
||||
ea = stringItem.ea if stringItem.ea else idc.BADADDR
|
||||
idaRTTIStrings[inputMD5].update({ s: ea })
|
||||
rttiStrings = idaRTTIStrings[inputMD5]
|
||||
return rttiStrings
|
||||
|
||||
@cache
|
||||
def FindPatternInRange(inputMD5: bytes, pattern: str, start: int, size: int, end: int = 0) -> int:
|
||||
if not end:
|
||||
end = start + size
|
||||
|
||||
compiledIDAPattern = ida_bytes.compiled_binpat_vec_t()
|
||||
errorParsingIDAPattern = ida_bytes.parse_binpat_str(compiledIDAPattern, 0, pattern, 16, IDA_NALT_ENCODING)
|
||||
if errorParsingIDAPattern:
|
||||
return idc.BADADDR
|
||||
|
||||
patternAddr: int = ida_bytes.bin_search(start, end, compiledIDAPattern, ida_bytes.BIN_SEARCH_FORWARD)
|
||||
if patternAddr == idc.BADADDR:
|
||||
return idc.BADADDR
|
||||
|
||||
return patternAddr
|
||||
|
||||
@cache
|
||||
def FindAllPatternsInRange(inputMD5: bytes, pattern: str, start: int, size: int, end: int = 0) -> list[int]:
|
||||
"""Find all occurrences of a pattern within a memory range."""
|
||||
addresses: list[int] = []
|
||||
if not end:
|
||||
end = start + size
|
||||
|
||||
while start < end:
|
||||
patternAddr = FindPatternInRange(inputMD5, pattern, start, size, end)
|
||||
if patternAddr == idc.BADADDR:
|
||||
break
|
||||
|
||||
addresses.append(patternAddr)
|
||||
start = patternAddr + 8 # advance past found pattern
|
||||
|
||||
return addresses
|
@ -1,17 +1,20 @@
|
||||
import os
|
||||
import json
|
||||
from typing import Optional
|
||||
from functools import cache
|
||||
|
||||
from ExportClassH import Utils, IDAUtils, RTTIAnalyzer, Config
|
||||
from ExportClassH.ClassDefs import ParsedClass, ParsedFunction, ParsedParam
|
||||
from ExportClassH.ClassDefs import ParsedParam, ParsedClass, ParsedFunction, ParsedClassVar, DefaultPydanticSerializer
|
||||
|
||||
CLASS_TYPES = ["namespace", "class", "struct", "enum", "union"]
|
||||
FUNC_TYPES = ["function", "strippedVirtual", "basicVirtual", "virtual"]
|
||||
TYPES_OF_RETURN_TYPES = ["returnType", "classReturnType"]
|
||||
STD_CLASSES = ["std", "rapidjson"]
|
||||
|
||||
parsedClassesLookupDict: dict[str, ParsedClass] = {}
|
||||
parsedClassesDict: dict[str, ParsedClass] = {}
|
||||
unparsedDemangledExportedSigs: list[str]= []
|
||||
|
||||
@cache
|
||||
def GetTypeAndNameStr(fullName: str, returnFullName: bool = False) -> str:
|
||||
parts = Utils.ExtractTypeTokensFromString(fullName)
|
||||
if not parts:
|
||||
@ -24,6 +27,7 @@ def GetTypeAndNameStr(fullName: str, returnFullName: bool = False) -> str:
|
||||
return f"{parts[i]} {parts[i + 1] if not returnFullName else ' '.join(parts[i + 1:])}"
|
||||
return ""
|
||||
|
||||
@cache
|
||||
def SplitTypeFromName(fullName: str, returnFullName: bool = False) -> tuple[str, str]:
|
||||
typeAndNameStr = GetTypeAndNameStr(fullName, returnFullName)
|
||||
if not typeAndNameStr:
|
||||
@ -32,10 +36,10 @@ def SplitTypeFromName(fullName: str, returnFullName: bool = False) -> tuple[str,
|
||||
typeAndName = typeAndNameStr.split(maxsplit=1)
|
||||
return typeAndName[0], typeAndName[1]
|
||||
|
||||
def GetParsedParamsFromList(paramsList: list[str], type: str) -> list[ParsedParam]:
|
||||
def GetParsedParamsFromList(paramsList: list[str], paramType: str) -> list[ParsedParam]:
|
||||
params: list[ParsedParam] = []
|
||||
for i in range(len(paramsList)):
|
||||
typeOfParam: str = type
|
||||
typeOfParam: str = paramType
|
||||
classType, className = SplitTypeFromName(paramsList[i], True)
|
||||
nameOfParam: str = className
|
||||
parsedClassOfParam: Optional[ParsedClass] = None
|
||||
@ -43,9 +47,14 @@ def GetParsedParamsFromList(paramsList: list[str], type: str) -> list[ParsedPara
|
||||
if classType:
|
||||
typeOfParam = f"class{typeOfParam[0].upper()}{typeOfParam[1:]}"
|
||||
parsedClassOfParam = ParseClassStr(f"{classType} {className}")
|
||||
if parsedClassOfParam and parsedClassOfParam.parentNamespaces:
|
||||
parentNamespaces, parentClasses = ExtractParentNamespacesAndClasses(parsedClassOfParam.parentNamespaces)
|
||||
parsedClassOfParam.parentNamespaces = parentNamespaces
|
||||
parsedClassOfParam.parentClasses = parentClasses
|
||||
params.append(ParsedParam(type=typeOfParam, name=nameOfParam, parsedClassParam=parsedClassOfParam))
|
||||
return params
|
||||
|
||||
@cache
|
||||
def ExtractClassNameAndTemplateParams(templatedClassName: str) -> tuple[str, list[ParsedParam]]:
|
||||
className = templatedClassName
|
||||
templateParams: list[ParsedParam] = []
|
||||
@ -122,7 +131,8 @@ def ParseClassStr(clsStr: str) -> Optional[ParsedClass]:
|
||||
|
||||
virtualFuncDuplicateCounter: dict[tuple[str, str], int] = {}
|
||||
virtualFuncPlaceholderCounter: dict[tuple[str, str], int] = {}
|
||||
def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVirtualFuncs: bool = False) -> Optional[ParsedFunction]:
|
||||
|
||||
def ParseFuncStr(funcStr: str, parsedClassFullName: str = "", onlyVirtualFuncs: bool = False) -> Optional[ParsedFunction]:
|
||||
global virtualFuncDuplicateCounter
|
||||
global virtualFuncPlaceholderCounter
|
||||
|
||||
@ -164,7 +174,8 @@ def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVi
|
||||
# Extract parameters
|
||||
paramsStr = funcStr[paramsOpenParenIndex + 1:paramsCloseParenIndex]
|
||||
params = GetParsedParamsFromList(Utils.SplitByCommaOutsideTemplates(paramsStr), "param")
|
||||
parsedFunc.params = params
|
||||
if params and params[0] != "void":
|
||||
parsedFunc.params = params
|
||||
|
||||
# Check for const qualifier
|
||||
remainingInputAfterParamsParen = funcStr[paramsCloseParenIndex + 1:].strip()
|
||||
@ -212,13 +223,13 @@ def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVi
|
||||
parentNamespacesAndClasses = Utils.SplitByClassSeparatorOutsideTemplates(namespacesAndClasses)
|
||||
parentNamespaces, parentClasses = ExtractParentNamespacesAndClasses(parentNamespacesAndClasses)
|
||||
parsedFunc.parentNamespaces = parentNamespaces
|
||||
parsedFunc.parentNamespaces = parentClasses
|
||||
parsedFunc.parentClasses = parentClasses
|
||||
|
||||
# Handle duplicate function naming
|
||||
if isDuplicateFunc:
|
||||
if not parsedClass and not namespacesAndClasses:
|
||||
if not parsedClassFullName and not namespacesAndClasses:
|
||||
raise Exception("parsedClass variable not provided and namespacesAndClasses is empty for ParseFuncStr when func is duplicate")
|
||||
key = (parsedClass.fullClassName if parsedClass else namespacesAndClasses, funcStr)
|
||||
key = (parsedClassFullName if parsedClassFullName else namespacesAndClasses, funcStr)
|
||||
if key not in virtualFuncDuplicateCounter:
|
||||
virtualFuncDuplicateCounter[key] = 0
|
||||
virtualFuncDuplicateCounter[key] += 1
|
||||
@ -245,10 +256,10 @@ def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVi
|
||||
parsedFunc.returnTypes = returnTypes
|
||||
parsedFunc.funcName = funcName
|
||||
elif onlyVirtualFuncs and funcStr == "_purecall":
|
||||
if not parsedClass:
|
||||
if not parsedClassFullName:
|
||||
raise Exception("parsedClass variable not provided for ParseFuncStr when func is _purecall")
|
||||
|
||||
key = (parsedClass.fullClassName, funcStr)
|
||||
key = (parsedClassFullName, funcStr)
|
||||
if key not in virtualFuncPlaceholderCounter:
|
||||
virtualFuncPlaceholderCounter[key] = 0
|
||||
virtualFuncPlaceholderCounter[key] += 1
|
||||
@ -258,6 +269,69 @@ def ParseFuncStr(funcStr: str, parsedClass: Optional[ParsedClass] = None, onlyVi
|
||||
|
||||
return parsedFunc
|
||||
|
||||
def ParseClassVarStr(classVarStr: str) -> Optional[ParsedClassVar]:
|
||||
# Strip whitespace
|
||||
classVarStr = classVarStr.strip()
|
||||
if not classVarStr:
|
||||
return None
|
||||
|
||||
parsedClassVar = ParsedClassVar()
|
||||
parsedClassVar.fullClassVarSig = classVarStr
|
||||
|
||||
# Extract access modifier
|
||||
for keyword in ("public:", "protected:", "private:"):
|
||||
if classVarStr.startswith(keyword):
|
||||
parsedClassVar.access = keyword[:-1] # remove the colon
|
||||
classVarStr = classVarStr[len(keyword):].strip()
|
||||
break
|
||||
# Find parameters and const qualifier
|
||||
paramsOpenParenIndex = classVarStr.find('(')
|
||||
paramsCloseParenIndex = classVarStr.rfind(')')
|
||||
|
||||
# For class variables, we expect no parameters (i.e. no parentheses).
|
||||
if paramsOpenParenIndex == -1 and paramsCloseParenIndex == -1:
|
||||
varType = ""
|
||||
namespacesAndClasses = ""
|
||||
varName = ""
|
||||
|
||||
# Find the last space outside of angle brackets
|
||||
lastSpaceIndex = Utils.FindLastSpaceOutsideTemplates(classVarStr)
|
||||
|
||||
if lastSpaceIndex != -1:
|
||||
# Split at the last space outside angle brackets
|
||||
varType = classVarStr[:lastSpaceIndex].strip()
|
||||
classAndVarName = classVarStr[lastSpaceIndex+1:].strip()
|
||||
|
||||
# Find the last class separator outside of angle brackets
|
||||
lastClassSeparatorIndex = Utils.FindLastClassSeparatorOutsideTemplates(classAndVarName)
|
||||
|
||||
if lastClassSeparatorIndex != -1:
|
||||
namespacesAndClasses = classAndVarName[:lastClassSeparatorIndex]
|
||||
varName = classAndVarName[lastClassSeparatorIndex+2:]
|
||||
else:
|
||||
classParts = Utils.SplitByClassSeparatorOutsideTemplates(classAndVarName)
|
||||
namespacesAndClasses = "::".join(classParts[:-1]) if len(classParts) > 1 else ""
|
||||
varName = classParts[-1]
|
||||
else:
|
||||
return None
|
||||
|
||||
parsedClassVar.fullClassName = namespacesAndClasses
|
||||
parentNamespacesAndClasses = Utils.SplitByClassSeparatorOutsideTemplates(namespacesAndClasses)
|
||||
parentNamespaces, parentClasses = ExtractParentNamespacesAndClasses(parentNamespacesAndClasses)
|
||||
parsedClassVar.parentNamespaces = parentNamespaces
|
||||
parsedClassVar.parentClasses = parentClasses
|
||||
|
||||
varType = Utils.ReplaceIDATypes(varType)
|
||||
varType = Utils.CleanType(varType)
|
||||
varTypes = GetParsedParamsFromList(Utils.ExtractTypeTokensFromString(varType), "classVarType")
|
||||
parsedClassVar.varTypes = varTypes
|
||||
parsedClassVar.varName = varName
|
||||
else:
|
||||
return None
|
||||
|
||||
return parsedClassVar
|
||||
|
||||
@cache
|
||||
def ExtractAllClassSigsFromFuncSig(funcSig: str) -> list[str]:
|
||||
parts = Utils.ExtractTypeTokensFromString(funcSig)
|
||||
if not len(parts) > 1:
|
||||
@ -270,6 +344,7 @@ def ExtractAllClassSigsFromFuncSig(funcSig: str) -> list[str]:
|
||||
listOfClassSigs.append(f"{classType} {className}")
|
||||
return listOfClassSigs
|
||||
|
||||
@cache
|
||||
def ExtractMainClassSigFromFuncSig(funcSig: str) -> str:
|
||||
for keyword in ("public:", "protected:", "private:"):
|
||||
if funcSig.startswith(keyword):
|
||||
@ -323,24 +398,40 @@ def ExtractMainClassSigFromFuncSig(funcSig: str) -> str:
|
||||
|
||||
return f"{'class' if namespacesAndClasses.endswith(funcName) else 'namespace'} {namespacesAndClasses}" if namespacesAndClasses else ""
|
||||
|
||||
def BuildParsedClassesLookup(rootClasses: list[ParsedClass], lookupDict: dict[str, ParsedClass]):
|
||||
lookupDict = {}
|
||||
def build(parsedClasses: list[ParsedClass]):
|
||||
for parsedClass in parsedClasses:
|
||||
lookupDict[parsedClass.fullClassName] = parsedClass
|
||||
if parsedClass.childClasses:
|
||||
build(list(parsedClass.childClasses.values()))
|
||||
build(rootClasses)
|
||||
def FindParentClassRecursive(fullClassName: str, parsedClassesDict: dict[str, ParsedClass]) -> Optional[ParsedClass]:
|
||||
if fullClassName in parsedClassesDict:
|
||||
return parsedClassesDict[fullClassName]
|
||||
for parsedClass in parsedClassesDict.values():
|
||||
result = FindParentClassRecursive(fullClassName, parsedClass.childClasses)
|
||||
if result is not None:
|
||||
return result
|
||||
return None
|
||||
|
||||
def ParseAllClasses():
|
||||
global parsedClassesLookupDict
|
||||
global parsedClassesDict
|
||||
parsedClassesLookupDict = {}
|
||||
parsedClassesDict = {}
|
||||
def MoveChildClasses(parsedClassesDict: dict[str, ParsedClass]):
|
||||
# Find and move child classes to parent classes
|
||||
parsedClassesDictCopy = parsedClassesDict.copy()
|
||||
for childClass in parsedClassesDictCopy.values():
|
||||
if not childClass.parentNamespaces and not childClass.parentClasses:
|
||||
continue
|
||||
|
||||
parentFullName = childClass.fullClassName
|
||||
lastClassSeparator = Utils.FindLastClassSeparatorOutsideTemplates(parentFullName)
|
||||
if lastClassSeparator:
|
||||
parentFullName = parentFullName[:lastClassSeparator]
|
||||
|
||||
parentClass = FindParentClassRecursive(parentFullName, parsedClassesDict)
|
||||
if not parentClass:
|
||||
continue
|
||||
|
||||
parentClass.childClasses[childClass.fullClassName] = childClass
|
||||
del parsedClassesDict[childClass.fullClassName]
|
||||
|
||||
def ParseAllClasses(parsedClassesDict: dict[str, ParsedClass]):
|
||||
# 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:
|
||||
global unparsedDemangledExportedSigs
|
||||
unparsedDemangledExportedSigs = IDAUtils.GetDemangledExportedSigs(Config.INPUT_MD5)
|
||||
|
||||
for demangledFuncSig in unparsedDemangledExportedSigs:
|
||||
listOfExtractedClassSigs = ExtractAllClassSigsFromFuncSig(demangledFuncSig)
|
||||
for clsSig in listOfExtractedClassSigs:
|
||||
parsedClass = ParseClassStr(clsSig)
|
||||
@ -354,7 +445,7 @@ def ParseAllClasses():
|
||||
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:
|
||||
for demangledFuncSig in unparsedDemangledExportedSigs:
|
||||
extractedMainClassSig = ExtractMainClassSigFromFuncSig(demangledFuncSig)
|
||||
parsedClass = ParseClassStr(extractedMainClassSig)
|
||||
if not parsedClass:
|
||||
@ -399,34 +490,6 @@ def ParseAllClasses():
|
||||
if (parentClasses or parsedClass.templateParams) and parsedClass.type == "namespace":
|
||||
parsedClass.type = "class"
|
||||
|
||||
# Find and move child classes to parent classes
|
||||
parsedClassesCopy = list(parsedClassesDict.values())
|
||||
for parsedClass in parsedClassesCopy:
|
||||
if not parsedClass.parentNamespaces and not parsedClass.parentClasses:
|
||||
continue
|
||||
|
||||
parentName = ""
|
||||
if parsedClass.parentClasses:
|
||||
parentName = parsedClass.parentClasses[-1]
|
||||
elif parsedClass.parentNamespaces:
|
||||
parentName = parsedClass.parentNamespaces[-1]
|
||||
if not parentName:
|
||||
continue
|
||||
|
||||
parentClass = None
|
||||
if parsedClass.parentClasses:
|
||||
parentClass = next((parentClass for parentClass in parsedClassesCopy if parentClass.name == parentName and parentClass.parentClasses == parsedClass.parentClasses[:-1]), None)
|
||||
elif parsedClass.parentNamespaces:
|
||||
parentClass = next((parentClass for parentClass in parsedClassesCopy if parentClass.name == parentName and parentClass.parentNamespaces == parsedClass.parentNamespaces[:-1]), None)
|
||||
if not parentClass:
|
||||
continue
|
||||
|
||||
parentClass.childClasses[parsedClass.fullClassName] = parsedClass
|
||||
del parsedClassesDict[parsedClass.fullClassName]
|
||||
|
||||
# Build the lookup for parsed classes, so we can have faster and more efficient lookup times
|
||||
BuildParsedClassesLookup(list(parsedClassesDict.values()), parsedClassesLookupDict)
|
||||
|
||||
def CreateParamNamesForVTFunc(parsedFunc: ParsedFunction, skipFirstParam: bool) -> str:
|
||||
paramsList: list[str] = [param.name for param in parsedFunc.params if param.name]
|
||||
if len(paramsList) == 1 and paramsList[0] == "void":
|
||||
@ -441,14 +504,9 @@ def CreateParamNamesForVTFunc(parsedFunc: ParsedFunction, skipFirstParam: bool)
|
||||
return newParams
|
||||
|
||||
def ParseClassVTFuncs(parsedClass: ParsedClass):
|
||||
# Parse child classes first
|
||||
for parsedChildClass in parsedClass.childClasses.values():
|
||||
ParseClassVTFuncs(parsedChildClass)
|
||||
|
||||
# Parse root class
|
||||
for (demangledFuncSig, rawType) in RTTIAnalyzer.GetDemangledVTableFuncSigs(parsedClass):
|
||||
for (demangledFuncSig, rawType) in RTTIAnalyzer.GetDemangledVTableFuncSigs(Config.INPUT_MD5, tuple(parsedClass.parentNamespaces + parsedClass.parentClasses), parsedClass.name):
|
||||
if rawType:
|
||||
parsedFunc = ParseFuncStr(rawType, parsedClass, True)
|
||||
parsedFunc = ParseFuncStr(rawType, parsedClass.fullClassName, True)
|
||||
if not parsedFunc:
|
||||
continue
|
||||
|
||||
@ -458,7 +516,7 @@ def ParseClassVTFuncs(parsedClass: ParsedClass):
|
||||
returnTypesStr = ' '.join(returnTypes)
|
||||
demangledFuncSig = f"{'DUPLICATE_FUNC ' if demangledFuncSig.startswith('DUPLICATE_FUNC') else ''}IDA_GEN_PARSED virtual {returnTypesStr} {demangledFuncSig.removeprefix('DUPLICATE_FUNC').strip()}({newParamTypes})"
|
||||
elif demangledFuncSig.startswith("DUPLICATE_FUNC"):
|
||||
parsedFunc = ParseFuncStr(demangledFuncSig.removeprefix("DUPLICATE_FUNC").strip(), parsedClass, True)
|
||||
parsedFunc = ParseFuncStr(demangledFuncSig.removeprefix("DUPLICATE_FUNC").strip(), parsedClass.fullClassName, True)
|
||||
if not parsedFunc:
|
||||
continue
|
||||
|
||||
@ -468,34 +526,90 @@ def ParseClassVTFuncs(parsedClass: ParsedClass):
|
||||
returnTypesStr = ' '.join(returnTypes)
|
||||
demangledFuncSig = f"DUPLICATE_FUNC {returnTypesStr} {parsedFunc.funcName}({newParamTypes})"
|
||||
|
||||
parsedFunc = ParseFuncStr(demangledFuncSig, parsedClass, True)
|
||||
parsedFunc = ParseFuncStr(demangledFuncSig, parsedClass.fullClassName, True)
|
||||
if not parsedFunc:
|
||||
continue
|
||||
|
||||
if parsedClass.type == "namespace":
|
||||
parsedClass.type = "class"
|
||||
parsedClass.functions.append(parsedFunc)
|
||||
|
||||
def ParseAllClassVTFuncs():
|
||||
# Add dependency classes by going through the func class name, return types and params
|
||||
if parsedFunc.fullClassName and parsedFunc.fullClassName not in parsedClass.classDependencies:
|
||||
parsedClass.classDependencies.append(parsedFunc.fullClassName)
|
||||
for param in parsedFunc.params:
|
||||
if param.parsedClassParam and param.parsedClassParam.fullClassName and param.parsedClassParam.fullClassName not in parsedClass.classDependencies:
|
||||
parsedClass.classDependencies.append(param.parsedClassParam.fullClassName)
|
||||
for returnType in parsedFunc.returnTypes:
|
||||
if returnType.parsedClassParam and returnType.parsedClassParam.fullClassName and returnType.parsedClassParam.fullClassName not in parsedClass.classDependencies:
|
||||
parsedClass.classDependencies.append(returnType.parsedClassParam.fullClassName)
|
||||
|
||||
# Append parsed func to class
|
||||
parsedClass.virtualFunctions.append(parsedFunc)
|
||||
|
||||
def ParseAllClassVTFuncs(parsedClassesDict: dict[str, ParsedClass]):
|
||||
for parsedClass in parsedClassesDict.values():
|
||||
ParseClassVTFuncs(parsedClass)
|
||||
|
||||
def ParseAllClassFuncs():
|
||||
global parsedClassesLookupDict
|
||||
for demangledExportedSig in IDAUtils.GetDemangledExportedSigs():
|
||||
parsedFunc = ParseFuncStr(demangledExportedSig, None, False)
|
||||
def ParseAllClassFuncs(parsedClassesDict: dict[str, ParsedClass]):
|
||||
global unparsedDemangledExportedSigs
|
||||
|
||||
for demangledExportedSig in unparsedDemangledExportedSigs.copy():
|
||||
parsedFunc = ParseFuncStr(demangledExportedSig, "", False)
|
||||
if not parsedFunc or not parsedFunc.fullClassName:
|
||||
continue
|
||||
|
||||
parsedClass = parsedClassesLookupDict.get(parsedFunc.fullClassName)
|
||||
parsedClass = parsedClassesDict.get(parsedFunc.fullClassName)
|
||||
if not parsedClass:
|
||||
continue
|
||||
|
||||
# Add dependency classes by going through the return types and params
|
||||
for param in parsedFunc.params:
|
||||
if param.parsedClassParam and param.parsedClassParam.fullClassName and param.parsedClassParam.fullClassName not in parsedClass.classDependencies:
|
||||
parsedClass.classDependencies.append(param.parsedClassParam.fullClassName)
|
||||
for returnType in parsedFunc.returnTypes:
|
||||
if returnType.parsedClassParam and returnType.parsedClassParam.fullClassName and returnType.parsedClassParam.fullClassName not in parsedClass.classDependencies:
|
||||
parsedClass.classDependencies.append(returnType.parsedClassParam.fullClassName)
|
||||
|
||||
# Append parsed func to class
|
||||
parsedClass.functions.append(parsedFunc)
|
||||
unparsedDemangledExportedSigs.remove(demangledExportedSig)
|
||||
|
||||
def ParseAllClassVars(parsedClassesDict: dict[str, ParsedClass]):
|
||||
global unparsedDemangledExportedSigs
|
||||
|
||||
for demangledExportedSig in unparsedDemangledExportedSigs.copy():
|
||||
parsedClassVar = ParseClassVarStr(demangledExportedSig)
|
||||
if not parsedClassVar or not parsedClassVar.fullClassName:
|
||||
continue
|
||||
|
||||
parsedClass = parsedClassesDict.get(parsedClassVar.fullClassName)
|
||||
if not parsedClass:
|
||||
continue
|
||||
|
||||
parsedClass.functions.append(parsedFunc)
|
||||
# Add dependency classes by going through the var types
|
||||
for varType in parsedClassVar.varTypes:
|
||||
if varType.parsedClassParam and varType.parsedClassParam.fullClassName and varType.parsedClassParam.fullClassName not in parsedClass.classDependencies:
|
||||
parsedClass.classDependencies.append(varType.parsedClassParam.fullClassName)
|
||||
|
||||
def GetAllParsedClasses():
|
||||
ParseAllClasses()
|
||||
ParseAllClassVTFuncs()
|
||||
ParseAllClassFuncs()
|
||||
with open(Config.PARSED_CLASSES_OUTPUT_FILE, 'w') as fileStream:
|
||||
json.dump(parsedClassesDict, fileStream, indent=4)
|
||||
# Append parsed func to class
|
||||
parsedClass.classVars.append(parsedClassVar)
|
||||
unparsedDemangledExportedSigs.remove(demangledExportedSig)
|
||||
|
||||
def GetAllParsedClasses() -> dict[str, ParsedClass]:
|
||||
global parsedClassesDict
|
||||
|
||||
if os.path.exists(Config.PARSED_CLASSES_OUTPUT_FILE) and not parsedClassesDict:
|
||||
with open(Config.PARSED_CLASSES_OUTPUT_FILE, 'r') as fileStream:
|
||||
data = json.load(fileStream)
|
||||
parsedClassesDict = { key: ParsedClass.model_validate(value) for key, value in data.items() }
|
||||
elif not parsedClassesDict:
|
||||
parsedClassesDict.clear()
|
||||
ParseAllClasses(parsedClassesDict)
|
||||
ParseAllClassVTFuncs(parsedClassesDict)
|
||||
ParseAllClassFuncs(parsedClassesDict)
|
||||
ParseAllClassVars(parsedClassesDict)
|
||||
#MoveChildClasses(parsedClassesDict)
|
||||
with open(Config.PARSED_CLASSES_OUTPUT_FILE, 'w') as fileStream:
|
||||
fileStream.write(json.dumps(parsedClassesDict, indent=4, default=DefaultPydanticSerializer))
|
||||
|
||||
return parsedClassesDict
|
@ -7,14 +7,13 @@ def Main():
|
||||
UI.OpenMainDlg()
|
||||
|
||||
# Reload modules to apply any changes
|
||||
from ExportClassH import Config, Utils, IDAUtils, ClassDefs, JSONGen, RTTIAnalyzer#, ClassParser, HeaderGen, ProjectManager
|
||||
from ExportClassH import Config, Utils, IDAUtils, ClassDefs, JSONGen, RTTIAnalyzer, HeaderGen#, ProjectManager
|
||||
importlib.reload(Config)
|
||||
importlib.reload(Utils)
|
||||
importlib.reload(IDAUtils)
|
||||
importlib.reload(ClassDefs)
|
||||
importlib.reload(JSONGen)
|
||||
importlib.reload(RTTIAnalyzer)
|
||||
# importlib.reload(ClassParser)
|
||||
# importlib.reload(HeaderGen)
|
||||
importlib.reload(HeaderGen)
|
||||
# importlib.reload(ProjectManager)
|
||||
importlib.reload(UI)
|
@ -1,14 +1,13 @@
|
||||
import struct
|
||||
import idc
|
||||
import idaapi
|
||||
import ida_bytes
|
||||
import ida_ida
|
||||
import idautils
|
||||
import ida_hexrays
|
||||
from functools import cache
|
||||
|
||||
from ExportClassH import Utils
|
||||
from ExportClassH.ClassDefs import ParsedClass
|
||||
from ExportClassH import IDAUtils, Config
|
||||
|
||||
def GetVTablePtr(targetClass: ParsedClass, targetClassRTTIName: str = "") -> int:
|
||||
@cache
|
||||
def GetVTablePtr(inputMD5: bytes, parentNamespacesClasses: tuple[str, ...], targetClassName: str, targetClassRTTIName: str = "") -> int:
|
||||
"""
|
||||
Find vtable pointer for a class using RTTI information.
|
||||
Supports both simple class names and namespaced class names.
|
||||
@ -16,86 +15,55 @@ def GetVTablePtr(targetClass: ParsedClass, targetClassRTTIName: str = "") -> int
|
||||
|
||||
Returns the vtable pointer (an integer) or 0 if not found.
|
||||
"""
|
||||
baseDLLAddr: int = idaapi.get_imagebase()
|
||||
|
||||
# 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(tuple(targetClass.parentNamespaces + targetClass.parentClasses), targetClass.name)
|
||||
typeDescriptorName: str = IDAUtils.GetMangledTypePrefix(parentNamespacesClasses, targetClassName)
|
||||
else:
|
||||
# Use the provided RTTI name directly
|
||||
typeDescriptorName: str = targetClassRTTIName
|
||||
|
||||
# Search for the RTTI type descriptor
|
||||
typeDescriptorBytes: bytes = typeDescriptorName.encode('ascii')
|
||||
idaPattern: str = Utils.BytesToIDAPattern(typeDescriptorBytes)
|
||||
|
||||
# Search in .rdata
|
||||
rdataStartAddr, rdataSize = Utils.GetSectionInfo(".rdata")
|
||||
if not rdataStartAddr:
|
||||
rttiStringsList = IDAUtils.GetIDARTTIStringsList(Config.INPUT_MD5)
|
||||
if not rttiStringsList:
|
||||
return 0
|
||||
|
||||
# Look for the type descriptor
|
||||
compiledIDAPattern = ida_bytes.compiled_binpat_vec_t()
|
||||
errorParsingIDAPattern = ida_bytes.parse_binpat_str(compiledIDAPattern, 0, idaPattern, 16, Utils.IDA_NALT_ENCODING)
|
||||
if errorParsingIDAPattern:
|
||||
return 0
|
||||
|
||||
typeDescriptorPatternAddr: int = ida_bytes.bin_search(rdataStartAddr, ida_ida.cvar.inf.max_ea, compiledIDAPattern, ida_bytes.BIN_SEARCH_FORWARD)
|
||||
if typeDescriptorPatternAddr == idc.BADADDR:
|
||||
typeDescriptorAddr = rttiStringsList.get(typeDescriptorName)
|
||||
if not typeDescriptorAddr or typeDescriptorAddr == idc.BADADDR:
|
||||
return 0
|
||||
|
||||
# Adjust to get RTTI type descriptor
|
||||
rttiTypeDescriptorAddr: int = typeDescriptorPatternAddr - 0x10
|
||||
rttiTypeDescriptorAddr: int = typeDescriptorAddr - 0x10
|
||||
xrefsToRTTITypeDescriptor = idautils.DataRefsTo(rttiTypeDescriptorAddr)
|
||||
if not xrefsToRTTITypeDescriptor:
|
||||
return 0
|
||||
|
||||
# Compute offset relative to base address
|
||||
rttiTypeDescriptorOffset: int = rttiTypeDescriptorAddr - baseDLLAddr
|
||||
rttiTypeDescriptorOffsetBytes: bytes = struct.pack("<I", rttiTypeDescriptorOffset)
|
||||
rttiTypeDescriptorOffsetPattern: str = Utils.BytesToIDAPattern(rttiTypeDescriptorOffsetBytes)
|
||||
|
||||
# Search for references to this offset
|
||||
xrefs: list[int] = Utils.FindAllPatternsInRange(rttiTypeDescriptorOffsetPattern, rdataStartAddr, rdataSize)
|
||||
|
||||
# Analyze each reference to find the vtable
|
||||
for xref in xrefs:
|
||||
xref: int
|
||||
|
||||
for xrefToRTTITypeDescriptor in xrefsToRTTITypeDescriptor:
|
||||
# Check offset from class
|
||||
offsetFromClass: int = idc.get_wide_dword(xref - 8)
|
||||
offsetFromClass: int = idc.get_wide_dword(xrefToRTTITypeDescriptor - 0x8)
|
||||
if offsetFromClass:
|
||||
continue
|
||||
|
||||
# Get object locator
|
||||
objectLocatorOffsetAddr: int = xref - 0xC
|
||||
|
||||
# Look for references to the object locator
|
||||
objectLocatorBytes: bytes = struct.pack("<Q", objectLocatorOffsetAddr)
|
||||
objectLocatorPattern: str = Utils.BytesToIDAPattern(objectLocatorBytes)
|
||||
|
||||
compiledIDAPattern = ida_bytes.compiled_binpat_vec_t()
|
||||
errorParsingIDAPattern = ida_bytes.parse_binpat_str(compiledIDAPattern, 0, objectLocatorPattern, 16, Utils.IDA_NALT_ENCODING)
|
||||
if errorParsingIDAPattern:
|
||||
continue
|
||||
|
||||
objectLocatorAddr: int = ida_bytes.bin_search(rdataStartAddr, ida_ida.cvar.inf.max_ea, compiledIDAPattern, ida_bytes.BIN_SEARCH_FORWARD)
|
||||
if objectLocatorAddr == idc.BADADDR:
|
||||
continue
|
||||
|
||||
# Vtable pointer is at (objectLocatorAddr + 0x8)
|
||||
vtableAddr: int = objectLocatorAddr + 8
|
||||
if vtableAddr <= 8:
|
||||
continue
|
||||
|
||||
return vtableAddr
|
||||
objectLocatorAddr: int = xrefToRTTITypeDescriptor - 0xC
|
||||
xrefsToObjectLocator = idautils.DataRefsTo(objectLocatorAddr)
|
||||
if not xrefsToObjectLocator:
|
||||
return 0
|
||||
|
||||
for xrefToObjectLocator in xrefsToObjectLocator:
|
||||
# Vtable pointer is at (objectLocatorAddr + 0x8)
|
||||
vtableAddr: int = xrefToObjectLocator + 0x8
|
||||
if vtableAddr <= 0x8:
|
||||
break
|
||||
return vtableAddr
|
||||
|
||||
return 0
|
||||
|
||||
def GetDemangledVTableFuncSigs(targetClass: ParsedClass, targetClassRTTIName: str = "") -> list[tuple[str, str]]:
|
||||
@cache
|
||||
def GetDemangledVTableFuncSigs(inputMD5: bytes, parentNamespacesClasses: tuple[str, ...], targetClassName: str, 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)
|
||||
vtablePtr: int = GetVTablePtr(inputMD5, parentNamespacesClasses, targetClassName, targetClassRTTIName)
|
||||
if not vtablePtr:
|
||||
return []
|
||||
|
||||
@ -112,7 +80,7 @@ def GetDemangledVTableFuncSigs(targetClass: ParsedClass, targetClassRTTIName: st
|
||||
break
|
||||
|
||||
funcSig: str = idc.get_func_name(ptr)
|
||||
demangledFuncSig: str = Utils.DemangleSig(funcSig)
|
||||
demangledFuncSig: str = IDAUtils.DemangleSig(funcSig)
|
||||
demangledFuncSig = demangledFuncSig if demangledFuncSig else funcSig
|
||||
rawType: str = ""
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import os
|
||||
import json
|
||||
import ida_kernwin
|
||||
import idc
|
||||
import cProfile
|
||||
|
||||
from ExportClassH import Config, JSONGen
|
||||
from ExportClassH import Config, HeaderGen
|
||||
|
||||
def SetConfigVars(settings):
|
||||
Config.PROJECT_INCLUDES_PATH = settings["PROJECT_INCLUDES_PATH"]
|
||||
@ -10,12 +12,12 @@ def SetConfigVars(settings):
|
||||
Config.LAST_CLICKED_RADIO = settings["LAST_CLICKED_RADIO"]
|
||||
|
||||
Config.HEADER_OUTPUT_PATH = os.path.join(Config.OUTPUT_PATH, "generated")
|
||||
Config.CACHE_OUTPUT_PATH = os.path.join(Config.OUTPUT_PATH, "cache")
|
||||
Config.PARSED_VARS_CACHE_FILENAME = os.path.join(Config.CACHE_OUTPUT_PATH, "parsedClassVarsByClass.cache")
|
||||
Config.PARSED_FUNCS_CACHE_FILENAME = os.path.join(Config.CACHE_OUTPUT_PATH, "parsedFuncsByClass.cache")
|
||||
|
||||
# Load settings from file
|
||||
def LoadConfig():
|
||||
def LoadConfig() -> dict:
|
||||
Config.INPUT_MD5 = idc.retrieve_input_file_md5()
|
||||
if not Config.INPUT_MD5:
|
||||
raise Exception("Failed retreiving input file MD5")
|
||||
if os.path.exists(Config.CONFIG_FILE):
|
||||
with open(Config.CONFIG_FILE, "r") as f:
|
||||
loadedJson = json.load(f)
|
||||
@ -73,11 +75,12 @@ Export Class to C++ Header
|
||||
|
||||
{FormChangeCb}
|
||||
|
||||
<##Update Project Code:{r_update}>
|
||||
<##Generate Class Code:{r_generate}>
|
||||
<##Update Project:{r_update}>
|
||||
<##Generate Classes:{r_classesGenerate}>
|
||||
<##Generate Class:{r_classGenerate}>
|
||||
<##Settings:{r_settings}>{radioGroup}>
|
||||
""", {
|
||||
'radioGroup': ida_kernwin.Form.RadGroupControl(("r_update", "r_generate", "r_settings")),
|
||||
'radioGroup': ida_kernwin.Form.RadGroupControl(("r_update", "r_classesGenerate", "r_classGenerate", "r_settings")),
|
||||
'FormChangeCb': ida_kernwin.Form.FormChangeCb(self.OnFormChange),
|
||||
})
|
||||
|
||||
@ -116,17 +119,19 @@ def OpenMainDlg():
|
||||
SaveConfig()
|
||||
|
||||
if selectedOption == 0:
|
||||
print("[INFO] Update Project Code selected!")
|
||||
print("[INFO] Update Project selected!")
|
||||
#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))
|
||||
JSONGen.GetAllParsedClasses()
|
||||
print("[INFO] Generate Classes selected!")
|
||||
cProfile.runctx('HeaderGen.ExportClassHeaders()', globals(), locals(), 'cProfiler-data.dat')
|
||||
elif selectedOption == 2:
|
||||
print("[INFO] Generate Class selected!")
|
||||
targetClass = ida_kernwin.ask_str("", 0, "Enter target class name:")
|
||||
if not targetClass:
|
||||
print("No target class specified. Aborting.")
|
||||
return
|
||||
cProfile.runctx('HeaderGen.ExportClassHeader(targetClass)', globals(), locals(), 'cProfiler-data.dat')
|
||||
elif selectedOption == 3:
|
||||
print("[INFO] Settings selected!")
|
||||
OpenSettingsDlg() # Open settings when selected
|
||||
else:
|
||||
|
@ -1,25 +1,24 @@
|
||||
import re
|
||||
from functools import cache
|
||||
from typing import Tuple
|
||||
import ida_nalt
|
||||
import ida_bytes
|
||||
import idaapi
|
||||
import idautils
|
||||
import idc
|
||||
|
||||
IDA_NALT_ENCODING = ida_nalt.get_default_encoding_idx(ida_nalt.BPU_1B)
|
||||
_RE_BEFORE_POINTER = re.compile(r'\s+([*&])')
|
||||
_RE_AFTER_POINTER = re.compile(r'([*&])(?![\s*&])')
|
||||
_RE_COMMA = re.compile(r'\s*,\s*')
|
||||
_RE_AFTER_ANGLE = re.compile(r'<\s+')
|
||||
_RE_BEFORE_ANGLE = re.compile(r'\s+>')
|
||||
_RE_BEFORE_PUNC = re.compile(r'\s+([\),])')
|
||||
_RE_SPACES = re.compile(r'\s+')
|
||||
|
||||
@cache
|
||||
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*,\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 '>'
|
||||
type = re.sub(r'\s+([\),])', r'\1', type)
|
||||
type = re.sub(r'\s+', ' ', type) # Collapse multiple spaces
|
||||
return type.strip()
|
||||
def FixTypeSpacing(type_str: str) -> str:
|
||||
type_str = _RE_BEFORE_POINTER.sub(r'\1', type_str)
|
||||
type_str = _RE_AFTER_POINTER.sub(r'\1 ', type_str)
|
||||
type_str = _RE_COMMA.sub(', ', type_str)
|
||||
type_str = _RE_AFTER_ANGLE.sub('<', type_str)
|
||||
type_str = _RE_BEFORE_ANGLE.sub('>', type_str)
|
||||
type_str = _RE_BEFORE_PUNC.sub(r'\1', type_str)
|
||||
type_str = _RE_SPACES.sub(' ', type_str)
|
||||
return type_str.strip()
|
||||
|
||||
@cache
|
||||
def CleanDoubleSpaces(str: str) -> str:
|
||||
@ -78,6 +77,16 @@ def ExtractTypeTokensFromString(types: str) -> list[str]:
|
||||
# Filter out empty strings
|
||||
return [word.strip() for word in result if word]
|
||||
|
||||
@cache
|
||||
def ExtractParamNames(params: str) -> str:
|
||||
paramsList: list[str] = [param.strip() for param in params.split(',') if param.strip()]
|
||||
if len(paramsList) == 1 and paramsList[0] == "void":
|
||||
return ""
|
||||
|
||||
paramNames: list[str] = [param.split(" ")[-1].strip() for param in paramsList]
|
||||
newParams: str = ", ".join(paramNames)
|
||||
return newParams
|
||||
|
||||
@cache
|
||||
def SplitByCommaOutsideTemplates(params: str) -> list[str]:
|
||||
parts = []
|
||||
@ -165,67 +174,4 @@ def FindLastClassSeparatorOutsideTemplates(s: str) -> int:
|
||||
# Only if we're not inside a template.
|
||||
if depth == 0 and i > 0 and s[i-1:i+1] == "::":
|
||||
return i - 1 # return the index of the first colon
|
||||
return -1
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IDA util functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@cache
|
||||
def DemangleSig(sig: str) -> str:
|
||||
return idaapi.demangle_name(sig, idaapi.MNG_LONG_FORM)
|
||||
|
||||
@cache
|
||||
def GetMangledTypePrefix(namespaces: tuple[str, ...], className: str) -> str:
|
||||
"""
|
||||
Get the appropriate mangled type prefix for a class name.
|
||||
For class "X" this would be ".?AVX@@"
|
||||
For class "NS::X" this would be ".?AVX@NS@@"
|
||||
For templated classes, best to use get_mangled_name_for_template instead.
|
||||
"""
|
||||
if not namespaces:
|
||||
return f".?AV{className}@@"
|
||||
|
||||
# For namespaced classes, the format is .?AVClassName@Namespace@@
|
||||
# For nested namespaces, they are separated with @ in reverse order
|
||||
mangledNamespaces = "@".join(reversed(namespaces))
|
||||
return f".?AV{className}@{mangledNamespaces}@@"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# IDA pattern search utilities
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@cache
|
||||
def BytesToIDAPattern(data: bytes) -> str:
|
||||
"""Convert bytes to IDA-friendly hex pattern string."""
|
||||
return " ".join("{:02X}".format(b) for b in data)
|
||||
|
||||
def GetSectionInfo(sectionName: str) -> Tuple[int, int]:
|
||||
"""Get start address and size of a specified section."""
|
||||
for seg_ea in idautils.Segments():
|
||||
if idc.get_segm_name(seg_ea) == sectionName:
|
||||
start = seg_ea
|
||||
end = idc.get_segm_end(seg_ea)
|
||||
return start, end - start
|
||||
return 0, 0
|
||||
|
||||
def FindAllPatternsInRange(pattern: str, start: int, size: int) -> list[int]:
|
||||
"""Find all occurrences of a pattern within a memory range."""
|
||||
addresses: list[int] = []
|
||||
ea: int = start
|
||||
end: int = start + size
|
||||
|
||||
while ea < end:
|
||||
compiledIDAPattern = ida_bytes.compiled_binpat_vec_t()
|
||||
errorParsingIDAPattern = ida_bytes.parse_binpat_str(compiledIDAPattern, 0, pattern, 16, IDA_NALT_ENCODING)
|
||||
if errorParsingIDAPattern:
|
||||
return []
|
||||
|
||||
patternAddr: int = ida_bytes.bin_search(ea, end, compiledIDAPattern, ida_bytes.BIN_SEARCH_FORWARD)
|
||||
if patternAddr == idc.BADADDR:
|
||||
break
|
||||
|
||||
addresses.append(patternAddr)
|
||||
ea = patternAddr + 8 # advance past found pattern
|
||||
|
||||
return addresses
|
||||
return -1
|
BIN
_IDAScripts/cProfiler-data.dat
Normal file
BIN
_IDAScripts/cProfiler-data.dat
Normal file
Binary file not shown.
11156
_IDAScripts/generated/cbs.h
Normal file
11156
_IDAScripts/generated/cbs.h
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user