fix: correct ilspycmd output parsing and heap access
- Fix _TYPE_LINE_PATTERN regex to match ilspycmd format (no colon) - Fix UserStringHeap data access using getattr instead of name mangling - Add _decompile_to_stdout method for simple decompilation - Fix decompile to not always use -o flag when stdout is desired - Update test data to match actual ilspycmd output format
This commit is contained in:
parent
3c21b9d640
commit
609d97b86d
@ -194,15 +194,18 @@ class ILSpyWrapper:
|
|||||||
assembly_name=os.path.basename(request.assembly_path),
|
assembly_name=os.path.basename(request.assembly_path),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use TemporaryDirectory context manager for guaranteed cleanup (no race condition)
|
# When user specifies output_dir OR uses project mode, we need files
|
||||||
# when user doesn't specify an output directory
|
# Otherwise, we can get output directly from stdout
|
||||||
if request.output_dir:
|
if request.output_dir:
|
||||||
# User specified output directory - use it directly
|
# User specified output directory - use it directly
|
||||||
return await self._decompile_to_dir(request, request.output_dir)
|
return await self._decompile_to_dir(request, request.output_dir)
|
||||||
else:
|
elif request.create_project:
|
||||||
# Create a temporary directory with guaranteed cleanup
|
# Project mode needs temp directory for multiple files
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
return await self._decompile_to_dir(request, temp_dir)
|
return await self._decompile_to_dir(request, temp_dir)
|
||||||
|
else:
|
||||||
|
# Simple decompilation - get output from stdout (no -o flag)
|
||||||
|
return await self._decompile_to_stdout(request)
|
||||||
|
|
||||||
async def _decompile_to_dir(
|
async def _decompile_to_dir(
|
||||||
self, request: DecompileRequest, output_dir: str
|
self, request: DecompileRequest, output_dir: str
|
||||||
@ -312,6 +315,79 @@ class ILSpyWrapper:
|
|||||||
type_name=request.type_name,
|
type_name=request.type_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _decompile_to_stdout(self, request: DecompileRequest) -> DecompileResponse:
|
||||||
|
"""Decompile to stdout (no -o flag) for simple single-file decompilation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Decompilation request (output_dir must be None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decompilation response with source_code from stdout
|
||||||
|
"""
|
||||||
|
args = [request.assembly_path]
|
||||||
|
|
||||||
|
# Add language version
|
||||||
|
args.extend(["-lv", request.language_version.value])
|
||||||
|
|
||||||
|
# Add type filter if specified
|
||||||
|
if request.type_name:
|
||||||
|
args.extend(["-t", request.type_name])
|
||||||
|
|
||||||
|
# No -o flag - output goes to stdout
|
||||||
|
|
||||||
|
# Add IL code flag
|
||||||
|
if request.show_il_code:
|
||||||
|
args.append("-il")
|
||||||
|
|
||||||
|
# Add reference paths
|
||||||
|
for ref_path in request.reference_paths:
|
||||||
|
args.extend(["-r", ref_path])
|
||||||
|
|
||||||
|
# Add optimization flags
|
||||||
|
if request.remove_dead_code:
|
||||||
|
args.append("--no-dead-code")
|
||||||
|
|
||||||
|
if request.remove_dead_stores:
|
||||||
|
args.append("--no-dead-stores")
|
||||||
|
|
||||||
|
# Add IL sequence points flag
|
||||||
|
if request.show_il_sequence_points:
|
||||||
|
args.append("--il-sequence-points")
|
||||||
|
|
||||||
|
# Disable update check for automation
|
||||||
|
args.append("--disable-updatecheck")
|
||||||
|
|
||||||
|
assembly_name = os.path.splitext(os.path.basename(request.assembly_path))[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
return_code, stdout, stderr = await self._run_command(args)
|
||||||
|
|
||||||
|
if return_code == 0:
|
||||||
|
return DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
source_code=stdout,
|
||||||
|
output_path=None,
|
||||||
|
assembly_name=assembly_name,
|
||||||
|
type_name=request.type_name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error_msg = stderr or stdout or "Unknown error occurred"
|
||||||
|
return DecompileResponse(
|
||||||
|
success=False,
|
||||||
|
error_message=error_msg,
|
||||||
|
assembly_name=assembly_name,
|
||||||
|
type_name=request.type_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
logger.exception(f"Error during decompilation: {e}")
|
||||||
|
return DecompileResponse(
|
||||||
|
success=False,
|
||||||
|
error_message=str(e),
|
||||||
|
assembly_name=assembly_name,
|
||||||
|
type_name=request.type_name,
|
||||||
|
)
|
||||||
|
|
||||||
async def list_types(self, request: ListTypesRequest) -> ListTypesResponse:
|
async def list_types(self, request: ListTypesRequest) -> ListTypesResponse:
|
||||||
"""List types in a .NET assembly.
|
"""List types in a .NET assembly.
|
||||||
|
|
||||||
@ -359,8 +435,9 @@ class ILSpyWrapper:
|
|||||||
return ListTypesResponse(success=False, error_message=str(e))
|
return ListTypesResponse(success=False, error_message=str(e))
|
||||||
|
|
||||||
# Compiled regex for parsing ilspycmd list output
|
# Compiled regex for parsing ilspycmd list output
|
||||||
# Format: "TypeKind: FullTypeName" (e.g., "Class: MyNamespace.MyClass")
|
# Format: "TypeKind FullTypeName" (e.g., "Class MyNamespace.MyClass")
|
||||||
_TYPE_LINE_PATTERN = re.compile(r"^(\w+):\s*(.+)$")
|
# Only matches valid type kinds: Class, Interface, Struct, Enum, Delegate
|
||||||
|
_TYPE_LINE_PATTERN = re.compile(r"^(Class|Interface|Struct|Enum|Delegate)\s+(.+)$")
|
||||||
|
|
||||||
def _parse_types_output(self, output: str) -> list[TypeInfo]:
|
def _parse_types_output(self, output: str) -> list[TypeInfo]:
|
||||||
"""Parse the output from list types command.
|
"""Parse the output from list types command.
|
||||||
|
|||||||
@ -570,7 +570,8 @@ class MetadataReader:
|
|||||||
return
|
return
|
||||||
|
|
||||||
heap = pe.net.user_strings
|
heap = pe.net.user_strings
|
||||||
data = heap._ClrStream__data__ # Access the raw bytes
|
# Access raw bytes using getattr to avoid Python name mangling issues
|
||||||
|
data = getattr(heap, "__data__", None) # Raw heap bytes
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class TestILSpyWrapperTypeParsing:
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pytest.skip("ilspycmd not installed")
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
output = "Class: MyNamespace.MyClass"
|
output = "Class MyNamespace.MyClass"
|
||||||
types = wrapper._parse_types_output(output)
|
types = wrapper._parse_types_output(output)
|
||||||
|
|
||||||
assert len(types) == 1
|
assert len(types) == 1
|
||||||
@ -57,11 +57,11 @@ class TestILSpyWrapperTypeParsing:
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pytest.skip("ilspycmd not installed")
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
output = """Class: NS.ClassA
|
output = """Class NS.ClassA
|
||||||
Interface: NS.IService
|
Interface NS.IService
|
||||||
Struct: NS.MyStruct
|
Struct NS.MyStruct
|
||||||
Enum: NS.MyEnum
|
Enum NS.MyEnum
|
||||||
Delegate: NS.MyDelegate"""
|
Delegate NS.MyDelegate"""
|
||||||
|
|
||||||
types = wrapper._parse_types_output(output)
|
types = wrapper._parse_types_output(output)
|
||||||
assert len(types) == 5
|
assert len(types) == 5
|
||||||
@ -81,7 +81,7 @@ Delegate: NS.MyDelegate"""
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pytest.skip("ilspycmd not installed")
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
output = "Class: MyNamespace.Outer+Nested"
|
output = "Class MyNamespace.Outer+Nested"
|
||||||
types = wrapper._parse_types_output(output)
|
types = wrapper._parse_types_output(output)
|
||||||
|
|
||||||
assert len(types) == 1
|
assert len(types) == 1
|
||||||
@ -95,7 +95,7 @@ Delegate: NS.MyDelegate"""
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pytest.skip("ilspycmd not installed")
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
output = "Class: MyClass"
|
output = "Class MyClass"
|
||||||
types = wrapper._parse_types_output(output)
|
types = wrapper._parse_types_output(output)
|
||||||
|
|
||||||
assert len(types) == 1
|
assert len(types) == 1
|
||||||
@ -109,10 +109,10 @@ Delegate: NS.MyDelegate"""
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pytest.skip("ilspycmd not installed")
|
pytest.skip("ilspycmd not installed")
|
||||||
|
|
||||||
output = """Class: NS.Valid
|
output = """Class NS.Valid
|
||||||
This is not a valid line
|
This is not a valid line
|
||||||
Another invalid line
|
Another invalid line
|
||||||
Interface: NS.AlsoValid"""
|
Interface NS.AlsoValid"""
|
||||||
|
|
||||||
types = wrapper._parse_types_output(output)
|
types = wrapper._parse_types_output(output)
|
||||||
assert len(types) == 2
|
assert len(types) == 2
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user