backup script changes for JSONParser

This commit is contained in:
EricPlayZ
2025-03-16 10:25:30 +02:00
parent 0dd46bbabd
commit d40310fd80
8 changed files with 465 additions and 363 deletions

View File

@ -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)

View File

@ -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}

View 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)]

View File

@ -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))

View File

@ -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)

View File

@ -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]] = []

View File

@ -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

View File

@ -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)