UNPKG

il2cpp-dump-analyzer-mcp

Version:

Agentic RAG system for analyzing IL2CPP dump.cs files from Unity games

695 lines 30.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.EnhancedIL2CPPParser = void 0; const fs = __importStar(require("fs")); /** * Enhanced IL2CPP Dump Parser * Handles real IL2CPP dump format with comprehensive parsing capabilities * Supports TypeDefIndex, RVA/Offset, attributes, generic types, and nested classes */ class EnhancedIL2CPPParser { constructor() { this.content = ''; this.lines = []; this.imageMappings = new Map(); this.parseErrors = []; this.loaded = false; } /** * Load and parse an IL2CPP dump.cs file * @param filePath Path to the dump.cs file */ async loadFile(filePath) { try { console.log('Reading file:', filePath); const fileContent = await fs.promises.readFile(filePath, 'utf-8'); console.log('File content type:', typeof fileContent); console.log('File content length:', fileContent ? fileContent.length : 'undefined'); console.log('First 100 chars:', fileContent ? fileContent.substring(0, 100) : 'undefined'); this.content = fileContent; this.lines = this.content.split('\n'); this.parseErrors = []; this.loaded = true; // Parse the image map at the beginning of the file this.parseImageMappings(); } catch (error) { console.error('Error in loadFile:', error); this.parseErrors.push(`Failed to load file: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } /** * Load content directly from string (useful for testing) * @param content IL2CPP dump content as string */ loadContent(content) { this.content = content; this.lines = this.content.split('\n'); this.parseErrors = []; this.loaded = true; // Parse the image map at the beginning of the file this.parseImageMappings(); } /** * Check if the parser has loaded content */ isLoaded() { return this.loaded; } /** * Extract all IL2CPP constructs from the loaded content */ extractAllConstructs() { if (!this.loaded) { throw new Error('Parser not loaded. Call loadFile() first.'); } const classes = this.extractClasses(); const enums = this.extractEnums(); const interfaces = this.extractInterfaces(); // For now, return empty arrays for advanced constructs // These will be implemented in future iterations const delegates = []; const generics = []; const nestedTypes = []; const properties = []; const events = []; const constants = []; const operators = []; const indexers = []; const destructors = []; const extensionMethods = []; const statistics = this.calculateStatistics(classes, enums, interfaces, delegates, generics, nestedTypes, properties, events, constants, operators, indexers, destructors, extensionMethods); return { classes, enums, interfaces, delegates, generics, nestedTypes, properties, events, constants, operators, indexers, destructors, extensionMethods, imageMappings: this.imageMappings, statistics }; } /** * Parse image mappings from the beginning of the file * Format: // Image 0: holo-game.dll - 0 */ parseImageMappings() { const imageRegex = /\/\/ Image (\d+): (.+?) - (\d+)/; for (let i = 0; i < this.lines.length; i++) { const line = this.lines[i]; const trimmedLine = line.trim(); const match = trimmedLine.match(imageRegex); if (match) { const imageIndex = parseInt(match[1]); const imageName = match[2].trim(); this.imageMappings.set(imageIndex, imageName); } else if (trimmedLine.startsWith('// Namespace:')) { // Stop once we reach the first namespace declaration break; } } } /** * Extract all classes from the IL2CPP dump */ extractClasses() { const classes = []; let currentNamespace = ''; let i = 0; while (i < this.lines.length) { const line = this.lines[i].trim(); // Track namespace declarations if (line.startsWith('// Namespace:')) { currentNamespace = line.substring('// Namespace:'.length).trim(); i++; continue; } // Look for class/struct declarations with TypeDefIndex // Real format: [Serializable] public struct CustomAttackAnimOverrideConfig.OverrideConfig // TypeDefIndex: 5 if (line.includes('TypeDefIndex:') && /\b(class|struct|interface)\b/.test(line)) { try { // Extract TypeDefIndex const typeDefMatch = line.match(/TypeDefIndex:\s*(\d+)/); const typeDefIndex = typeDefMatch ? parseInt(typeDefMatch[1]) : 0; // Look for attributes on preceding lines let attributesStr = ''; let attributeLineIndex = i - 1; while (attributeLineIndex >= 0) { const prevLine = this.lines[attributeLineIndex].trim(); if (prevLine.startsWith('[') && prevLine.endsWith(']')) { attributesStr = prevLine + ' ' + attributesStr; attributeLineIndex--; } else if (prevLine === '' || prevLine.startsWith('// Namespace:')) { // Stop at empty lines or namespace declarations break; } else { break; } } // Also check for attributes on the same line (inline attributes) const inlineAttributeMatch = line.match(/^((?:\[.*?\]\s*)*)/); if (inlineAttributeMatch && inlineAttributeMatch[1]) { attributesStr = inlineAttributeMatch[1] + ' ' + attributesStr; } // Parse class declaration - handle the format before the TypeDefIndex comment const beforeComment = line.split('//')[0].trim(); const classMatch = beforeComment.match(/(public|internal|private|protected)?\s*(sealed\s+)?(static\s+)?(abstract\s+)?(class|struct|interface)\s+([^\s:]+)(?:\s*:\s*(.+))?/); if (classMatch) { const accessModifier = classMatch[1] || 'internal'; const classType = classMatch[5]; const className = classMatch[6]; const inheritance = classMatch[7] ? classMatch[7].trim() : ''; // Find the class body const { startLine, endLine } = this.findClassBody(i); if (endLine > startLine) { const classBody = this.lines.slice(startLine, endLine).join('\n'); // Parse class components const attributes = this.parseAttributes(attributesStr); const baseClass = this.parseBaseClass(inheritance); const interfaces = this.parseInterfaces(inheritance); const fields = this.parseFields(classBody); const methods = this.parseMethods(classBody); // Determine class properties const isStruct = classType === 'struct'; const isMonoBehaviour = inheritance.includes('MonoBehaviour'); classes.push({ name: className, namespace: currentNamespace, fullName: currentNamespace ? `${currentNamespace}.${className}` : className, baseClass, interfaces, fields, methods, isMonoBehaviour, isStruct, typeDefIndex, attributes }); i = endLine; } else { i++; } } else { i++; } } catch (error) { this.parseErrors.push(`Error parsing class at line ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`); i++; } } else { // Check for malformed class declarations (class/struct/interface without TypeDefIndex) if (/\b(class|struct|interface)\b/.test(line) && !line.includes('TypeDefIndex:')) { // This might be a malformed class declaration const beforeComment = line.split('//')[0].trim(); const malformedMatch = beforeComment.match(/(public|internal|private|protected)?\s*(sealed\s+)?(static\s+)?(abstract\s+)?(class|struct|interface)\s*([^\s:]*)/); if (malformedMatch) { const className = malformedMatch[6]; if (!className || className.includes('{') || className.includes('}')) { this.parseErrors.push(`Malformed class declaration at line ${i + 1}: missing or invalid class name`); } } } // Check for malformed method declarations if (line.includes('(') && !line.includes(')') && !line.includes('//')) { this.parseErrors.push(`Malformed method declaration at line ${i + 1}: missing closing parenthesis`); } i++; } } return classes; } /** * Extract all enums from the IL2CPP dump */ extractEnums() { const enums = []; let currentNamespace = ''; let i = 0; while (i < this.lines.length) { const line = this.lines[i].trim(); // Track namespace declarations if (line.startsWith('// Namespace:')) { currentNamespace = line.substring('// Namespace:'.length).trim(); i++; continue; } // Look for enum declarations with TypeDefIndex // Real format: public enum CameraFacingBillboardWithConstraints.LockAxis // TypeDefIndex: 7 if (line.includes('TypeDefIndex:') && /\benum\b/.test(line)) { try { // Extract TypeDefIndex const typeDefMatch = line.match(/TypeDefIndex:\s*(\d+)/); const typeDefIndex = typeDefMatch ? parseInt(typeDefMatch[1]) : 0; // Parse enum declaration - handle the format before the TypeDefIndex comment const beforeComment = line.split('//')[0].trim(); const enumMatch = beforeComment.match(/(public|internal|private|protected)?\s*enum\s+([^\s]+)/); if (enumMatch) { const enumName = enumMatch[2]; // Find the enum body const { startLine, endLine } = this.findClassBody(i); if (endLine > startLine) { const enumBody = this.lines.slice(startLine, endLine).join('\n'); const values = this.parseEnumValues(enumBody); enums.push({ name: enumName, namespace: currentNamespace, fullName: currentNamespace ? `${currentNamespace}.${enumName}` : enumName, values, typeDefIndex }); i = endLine; } else { i++; } } else { i++; } } catch (error) { this.parseErrors.push(`Error parsing enum at line ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`); i++; } } else { i++; } } return enums; } /** * Extract all interfaces from the IL2CPP dump */ extractInterfaces() { const interfaces = []; const interfaceRegex = /^(public|internal|private|protected)?\s*interface\s+([^\s{]+)\s*\/\/\s*TypeDefIndex:\s*(\d+)/; let currentNamespace = ''; let i = 0; while (i < this.lines.length) { const line = this.lines[i].trim(); // Track namespace declarations if (line.startsWith('// Namespace:')) { currentNamespace = line.substring('// Namespace:'.length).trim(); i++; continue; } // Look for interface declarations const interfaceMatch = line.match(interfaceRegex); if (interfaceMatch) { try { const interfaceName = interfaceMatch[2]; const typeDefIndex = parseInt(interfaceMatch[3]); // Find the interface body const { startLine, endLine } = this.findClassBody(i); if (endLine > startLine) { const interfaceBody = this.lines.slice(startLine, endLine).join('\n'); const methods = this.parseMethods(interfaceBody); interfaces.push({ name: interfaceName, namespace: currentNamespace, fullName: currentNamespace ? `${currentNamespace}.${interfaceName}` : interfaceName, methods, typeDefIndex }); i = endLine; } else { i++; } } catch (error) { this.parseErrors.push(`Error parsing interface at line ${i + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`); i++; } } else { i++; } } return interfaces; } /** * Find the body of a class/enum/interface by matching braces */ findClassBody(startIndex) { let braceCount = 0; let startLine = startIndex; let endLine = startIndex; // Find the opening brace while (endLine < this.lines.length && !this.lines[endLine].includes('{')) { endLine++; } if (endLine < this.lines.length) { const openingLine = this.lines[endLine]; // Check if both opening and closing braces are on the same line (e.g., "{}") const openBraces = (openingLine.match(/{/g) || []).length; const closeBraces = (openingLine.match(/}/g) || []).length; if (openBraces === closeBraces && openBraces > 0) { // Both braces on same line - this is an empty class body return { startLine, endLine: endLine + 1 }; } // Normal case: braces on separate lines braceCount = openBraces; endLine++; // Find the matching closing brace while (endLine < this.lines.length && braceCount > 0) { const currentLine = this.lines[endLine]; braceCount += (currentLine.match(/{/g) || []).length; braceCount -= (currentLine.match(/}/g) || []).length; endLine++; } } return { startLine, endLine }; } /** * Parse attributes from attribute string */ parseAttributes(attributesStr) { if (!attributesStr) return []; const attributes = []; const attrRegex = /\[(.*?)\]/g; let match; while ((match = attrRegex.exec(attributesStr)) !== null) { const attrContent = match[1].trim(); // Extract the attribute name (before any parentheses or parameters) const attrName = attrContent.split('(')[0].trim(); attributes.push(attrName); } return attributes; } /** * Parse base class from inheritance string */ parseBaseClass(inheritance) { if (!inheritance) return undefined; const parts = inheritance.split(',').map(p => p.trim()); if (parts.length > 0) { const baseClass = parts[0]; // Filter out interface names (typically start with 'I' and are capitalized) if (!baseClass.match(/^I[A-Z]/)) { return baseClass; } } return undefined; } /** * Parse interfaces from inheritance string */ parseInterfaces(inheritance) { if (!inheritance) return []; const parts = inheritance.split(',').map(p => p.trim()); // Skip the first part if it's a base class, return the rest as interfaces return parts.slice(1); } /** * Parse fields from class body */ parseFields(classBody) { const fields = []; const lines = classBody.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Skip comments, empty lines, and method declarations if (trimmedLine.startsWith('//') || !trimmedLine || trimmedLine.includes('(') || trimmedLine.includes('RVA:')) continue; // Field regex for real IL2CPP format: public float pixelScale; // 0x20 const fieldRegex = /^(public|private|protected|internal)?\s*(static\s+)?(readonly\s+)?([^;]+?)\s+([^;\s]+);\s*\/\/\s*(0x[0-9A-Fa-f]+)/; const match = trimmedLine.match(fieldRegex); if (match) { try { // Look for attributes on preceding lines let attributesStr = ''; let attributeLineIndex = i - 1; while (attributeLineIndex >= 0) { const prevLine = lines[attributeLineIndex].trim(); if (prevLine.startsWith('[') && prevLine.endsWith(']')) { attributesStr = prevLine + ' ' + attributesStr; attributeLineIndex--; } else if (prevLine === '' || prevLine.startsWith('//')) { // Stop at empty lines or comments break; } else { break; } } const accessModifier = match[1] || 'private'; const isStatic = !!match[2]; const isReadOnly = !!match[3]; const fieldType = match[4].trim(); const fieldName = match[5]; const offset = match[6]; const attributes = this.parseAttributes(attributesStr); fields.push({ name: fieldName, type: fieldType, isPublic: accessModifier === 'public', isPrivate: accessModifier === 'private', isStatic, isReadOnly, attributes, offset }); } catch (error) { this.parseErrors.push(`Error parsing field: ${trimmedLine}`); } } } return fields; } /** * Parse methods from class body */ parseMethods(classBody) { const methods = []; const lines = classBody.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Look for RVA comment lines that indicate method declarations // Real format: // RVA: 0x5BE44A0 Offset: 0x5BE34A0 VA: 0x5BE44A0 const rvaRegex = /^\/\/\s*RVA:\s*(0x[0-9A-Fa-f]+)\s*Offset:\s*(0x[0-9A-Fa-f]+)\s*VA:\s*(0x[0-9A-Fa-f]+)(?:\s*Slot:\s*(\d+))?/; const rvaMatch = trimmedLine.match(rvaRegex); if (rvaMatch) { const rva = rvaMatch[1]; const offset = rvaMatch[2]; const slot = rvaMatch[4] ? parseInt(rvaMatch[4]) : undefined; // Look for the method declaration on the next line if (i + 1 < lines.length) { const methodLine = lines[i + 1].trim(); // Real format: public void .ctor() { } // Real format: private void LateUpdate() { } // Real format: public T GetComponent<T>() where T : Component { } const methodRegex = /^(public|private|protected|internal)?\s*(override\s+)?(static\s+)?(virtual\s+)?(abstract\s+)?([^(]+?)\s+([^(]+)\((.*?)\)\s*(?:where\s+(.+?))?\s*\{/; const methodMatch = methodLine.match(methodRegex); if (methodMatch) { try { const accessModifier = methodMatch[1] || 'private'; const isOverride = !!methodMatch[2]; const isStatic = !!methodMatch[3]; const isVirtual = !!methodMatch[4]; const isAbstract = !!methodMatch[5]; const returnType = methodMatch[6].trim(); let methodName = methodMatch[7]; const parametersStr = methodMatch[8]; const whereClause = methodMatch[9]; // Parse generic constraints let isGeneric = false; let genericConstraints = ''; if (methodName.includes('<') && methodName.includes('>')) { isGeneric = true; // Extract the base method name (without generic type parameters) methodName = methodName.split('<')[0]; if (whereClause) { genericConstraints = `where ${whereClause}`; } } const parameters = this.parseParameters(parametersStr); methods.push({ name: methodName, returnType, parameters, isPublic: accessModifier === 'public', isPrivate: accessModifier === 'private', isStatic, isVirtual, isAbstract, isOverride, isGeneric, genericConstraints, slot, attributes: [], // Would need to parse method attributes rva, offset }); } catch (error) { this.parseErrors.push(`Error parsing method: ${methodLine}`); } } } } } return methods; } /** * Parse method parameters */ parseParameters(parametersStr) { if (!parametersStr || parametersStr.trim() === '') return []; const parameters = []; const paramParts = parametersStr.split(','); for (const part of paramParts) { const trimmed = part.trim(); if (trimmed) { const paramRegex = /^([^=]+?)(\s*=\s*(.+))?$/; const match = trimmed.match(paramRegex); if (match) { const typeAndName = match[1].trim(); const defaultValue = match[3]; // Split type and name const parts = typeAndName.split(/\s+/); if (parts.length >= 2) { const type = parts.slice(0, -1).join(' '); const name = parts[parts.length - 1]; parameters.push({ name, type }); } } } } return parameters; } /** * Parse enum values from enum body */ parseEnumValues(enumBody) { const values = []; const lines = enumBody.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); // Skip comments and empty lines if (trimmedLine.startsWith('//') || !trimmedLine) continue; // Enum value regex: public const EnumType VALUE_NAME = value; const enumValueRegex = /^public\s+const\s+[^=]+\s+([^=\s]+)\s*=\s*([^;]+);/; const match = trimmedLine.match(enumValueRegex); if (match) { const name = match[1]; const value = match[2].trim(); values.push({ name, value }); } } return values; } /** * Calculate comprehensive parsing statistics */ calculateStatistics(classes, enums, interfaces, delegates, generics, nestedTypes, properties, events, constants, operators, indexers, destructors, extensionMethods) { const totalConstructs = classes.length + enums.length + interfaces.length + delegates.length + generics.length + nestedTypes.length + properties.length + events.length + constants.length + operators.length + indexers.length + destructors.length + extensionMethods.length; const methodCount = classes.reduce((sum, cls) => sum + cls.methods.length, 0) + interfaces.reduce((sum, iface) => sum + iface.methods.length, 0) + generics.reduce((sum, gen) => sum + gen.methods.length, 0) + nestedTypes.reduce((sum, nested) => sum + nested.methods.length, 0) + extensionMethods.length; const fieldCount = classes.reduce((sum, cls) => sum + cls.fields.length, 0) + generics.reduce((sum, gen) => sum + gen.fields.length, 0) + nestedTypes.reduce((sum, nested) => sum + nested.fields.length, 0); const totalLines = this.lines.length; const processedLines = totalLines - this.parseErrors.length; const parsingCoverage = totalLines > 0 ? processedLines / totalLines : 0; const coveragePercentage = parsingCoverage * 100; // Count compiler generated types const compilerGeneratedCount = classes.filter(c => c.isCompilerGenerated).length + delegates.filter(d => d.isCompilerGenerated).length + generics.filter(g => g.isCompilerGenerated).length + nestedTypes.filter(n => n.isCompilerGenerated).length; return { totalConstructs, classCount: classes.length, enumCount: enums.length, interfaceCount: interfaces.length, delegateCount: delegates.length, genericCount: generics.length, nestedTypeCount: nestedTypes.length, propertyCount: properties.length, eventCount: events.length, constantCount: constants.length, operatorCount: operators.length, indexerCount: indexers.length, destructorCount: destructors.length, extensionMethodCount: extensionMethods.length, compilerGeneratedCount, coveragePercentage, methodCount, fieldCount, parseErrors: this.parseErrors.length, parsingCoverage }; } } exports.EnhancedIL2CPPParser = EnhancedIL2CPPParser; //# sourceMappingURL=enhanced-il2cpp-parser.js.map