maven-decoder-mcp 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maven_decoder_mcp/__init__.py +35 -0
- maven_decoder_mcp/config.py +108 -0
- maven_decoder_mcp/decompiler.py +329 -0
- maven_decoder_mcp/decompiler_paths.py +11 -0
- maven_decoder_mcp/maven_analyzer.py +437 -0
- maven_decoder_mcp/maven_decoder_server.py +829 -0
- maven_decoder_mcp-1.0.0.dist-info/METADATA +307 -0
- maven_decoder_mcp-1.0.0.dist-info/RECORD +13 -0
- maven_decoder_mcp-1.0.0.dist-info/WHEEL +5 -0
- maven_decoder_mcp-1.0.0.dist-info/entry_points.txt +3 -0
- maven_decoder_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
- maven_decoder_mcp-1.0.0.dist-info/top_level.txt +2 -0
- scripts/release.py +283 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Maven Decoder MCP Server
|
|
3
|
+
|
|
4
|
+
An MCP (Model Context Protocol) server for reading and decompiling Maven .m2 jar files.
|
|
5
|
+
Provides comprehensive Java project analysis including:
|
|
6
|
+
- Jar file exploration and analysis
|
|
7
|
+
- Java class decompilation (CFR, Procyon, javap)
|
|
8
|
+
- Maven dependency resolution
|
|
9
|
+
- Source code extraction
|
|
10
|
+
- Class structure analysis
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from maven_decoder_mcp import MavenDecoderServer
|
|
14
|
+
|
|
15
|
+
# Create and run server
|
|
16
|
+
server = MavenDecoderServer()
|
|
17
|
+
server.run()
|
|
18
|
+
|
|
19
|
+
Command line:
|
|
20
|
+
maven-decoder-mcp
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__version__ = "1.0.0"
|
|
24
|
+
__author__ = "Your Name"
|
|
25
|
+
__email__ = "your-email@example.com"
|
|
26
|
+
|
|
27
|
+
from .maven_decoder_server import MavenDecoderServer
|
|
28
|
+
from .decompiler import JavaDecompiler
|
|
29
|
+
from .maven_analyzer import MavenDependencyAnalyzer
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"MavenDecoderServer",
|
|
33
|
+
"JavaDecompiler",
|
|
34
|
+
"MavenDependencyAnalyzer",
|
|
35
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration module for Maven Decoder MCP Server
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
class Config:
|
|
10
|
+
"""Configuration settings for the Maven Decoder MCP Server"""
|
|
11
|
+
|
|
12
|
+
# Maven repository location
|
|
13
|
+
MAVEN_HOME: Path = Path.home() / ".m2" / "repository"
|
|
14
|
+
|
|
15
|
+
# Override Maven home from environment variable
|
|
16
|
+
if "M2_HOME" in os.environ:
|
|
17
|
+
MAVEN_HOME = Path(os.environ["M2_HOME"]) / "repository"
|
|
18
|
+
elif "MAVEN_REPO" in os.environ:
|
|
19
|
+
MAVEN_HOME = Path(os.environ["MAVEN_REPO"])
|
|
20
|
+
|
|
21
|
+
# Server configuration
|
|
22
|
+
SERVER_NAME: str = "maven-decoder"
|
|
23
|
+
SERVER_VERSION: str = "1.0.0"
|
|
24
|
+
|
|
25
|
+
# Decompiler settings
|
|
26
|
+
DECOMPILER_TIMEOUT: int = 30 # seconds
|
|
27
|
+
DECOMPILER_PRIORITY: list = ["cfr", "procyon", "fernflower", "javap"]
|
|
28
|
+
|
|
29
|
+
# Search and analysis limits
|
|
30
|
+
DEFAULT_ARTIFACT_LIMIT: int = 50
|
|
31
|
+
DEFAULT_SEARCH_LIMIT: int = 100
|
|
32
|
+
MAX_DEPENDENCY_DEPTH: int = 5
|
|
33
|
+
|
|
34
|
+
# Cache settings
|
|
35
|
+
ENABLE_CACHE: bool = True
|
|
36
|
+
CACHE_SIZE: int = 1000
|
|
37
|
+
CACHE_TTL: int = 3600 # seconds
|
|
38
|
+
|
|
39
|
+
# Logging configuration
|
|
40
|
+
LOG_LEVEL: str = os.environ.get("LOG_LEVEL", "INFO")
|
|
41
|
+
LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
42
|
+
|
|
43
|
+
# Performance settings
|
|
44
|
+
MAX_CONCURRENT_OPERATIONS: int = 10
|
|
45
|
+
JAR_ANALYSIS_TIMEOUT: int = 60 # seconds
|
|
46
|
+
|
|
47
|
+
# Feature flags
|
|
48
|
+
ENABLE_DECOMPILATION: bool = True
|
|
49
|
+
ENABLE_TRANSITIVE_DEPS: bool = True
|
|
50
|
+
ENABLE_BYTECODE_ANALYSIS: bool = False # Requires additional libraries
|
|
51
|
+
|
|
52
|
+
# File size limits (in bytes)
|
|
53
|
+
MAX_JAR_SIZE: int = 100 * 1024 * 1024 # 100MB
|
|
54
|
+
MAX_CLASS_SIZE: int = 1024 * 1024 # 1MB
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def validate(cls) -> bool:
|
|
58
|
+
"""Validate configuration settings"""
|
|
59
|
+
if not cls.MAVEN_HOME.exists():
|
|
60
|
+
print(f"Warning: Maven repository not found at {cls.MAVEN_HOME}")
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
if not cls.MAVEN_HOME.is_dir():
|
|
64
|
+
print(f"Error: Maven repository path is not a directory: {cls.MAVEN_HOME}")
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def get_maven_home(cls) -> Path:
|
|
71
|
+
"""Get the Maven repository home directory"""
|
|
72
|
+
return cls.MAVEN_HOME
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def set_maven_home(cls, path: str) -> None:
|
|
76
|
+
"""Set a custom Maven repository location"""
|
|
77
|
+
cls.MAVEN_HOME = Path(path)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def get_decompiler_config(cls) -> dict:
|
|
81
|
+
"""Get decompiler configuration"""
|
|
82
|
+
return {
|
|
83
|
+
"timeout": cls.DECOMPILER_TIMEOUT,
|
|
84
|
+
"priority": cls.DECOMPILER_PRIORITY,
|
|
85
|
+
"enabled": cls.ENABLE_DECOMPILATION
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def get_limits(cls) -> dict:
|
|
90
|
+
"""Get search and analysis limits"""
|
|
91
|
+
return {
|
|
92
|
+
"artifacts": cls.DEFAULT_ARTIFACT_LIMIT,
|
|
93
|
+
"search": cls.DEFAULT_SEARCH_LIMIT,
|
|
94
|
+
"dependency_depth": cls.MAX_DEPENDENCY_DEPTH,
|
|
95
|
+
"max_jar_size": cls.MAX_JAR_SIZE,
|
|
96
|
+
"max_class_size": cls.MAX_CLASS_SIZE
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Environment-specific configuration overrides
|
|
100
|
+
if os.environ.get("MAVEN_DECODER_ENV") == "development":
|
|
101
|
+
Config.LOG_LEVEL = "DEBUG"
|
|
102
|
+
Config.ENABLE_BYTECODE_ANALYSIS = True
|
|
103
|
+
Config.CACHE_TTL = 60 # Shorter cache in development
|
|
104
|
+
|
|
105
|
+
elif os.environ.get("MAVEN_DECODER_ENV") == "production":
|
|
106
|
+
Config.LOG_LEVEL = "WARNING"
|
|
107
|
+
Config.MAX_CONCURRENT_OPERATIONS = 20
|
|
108
|
+
Config.CACHE_SIZE = 5000
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Java Decompiler Integration for Maven Decoder MCP Server
|
|
3
|
+
|
|
4
|
+
Provides integration with various Java decompilers and bytecode analysis tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import os
|
|
10
|
+
import zipfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Dict, Any, List
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class JavaDecompiler:
|
|
18
|
+
"""Java bytecode decompiler and analyzer"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.available_decompilers = self._detect_decompilers()
|
|
22
|
+
|
|
23
|
+
def _detect_decompilers(self) -> Dict[str, str]:
|
|
24
|
+
"""Detect available decompilers on the system"""
|
|
25
|
+
decompilers = {}
|
|
26
|
+
|
|
27
|
+
# Check for CFR (free Java decompiler)
|
|
28
|
+
try:
|
|
29
|
+
cfr_paths = ['cfr.jar', 'decompilers/cfr.jar']
|
|
30
|
+
for cfr_path in cfr_paths:
|
|
31
|
+
if Path(cfr_path).exists():
|
|
32
|
+
result = subprocess.run(['java', '-jar', cfr_path, '--help'],
|
|
33
|
+
capture_output=True, text=True, timeout=5)
|
|
34
|
+
if result.returncode == 0:
|
|
35
|
+
decompilers['cfr'] = cfr_path
|
|
36
|
+
break
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
# Check for Fernflower (IntelliJ's decompiler)
|
|
41
|
+
try:
|
|
42
|
+
if Path('fernflower.jar').exists():
|
|
43
|
+
result = subprocess.run(['java', '-jar', 'fernflower.jar'],
|
|
44
|
+
capture_output=True, text=True, timeout=5)
|
|
45
|
+
decompilers['fernflower'] = 'fernflower.jar'
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# Check for Procyon
|
|
50
|
+
try:
|
|
51
|
+
procyon_paths = ['procyon-decompiler.jar', 'decompilers/procyon-decompiler.jar']
|
|
52
|
+
for procyon_path in procyon_paths:
|
|
53
|
+
if Path(procyon_path).exists():
|
|
54
|
+
result = subprocess.run(['java', '-jar', procyon_path, '--help'],
|
|
55
|
+
capture_output=True, text=True, timeout=5)
|
|
56
|
+
if result.returncode == 0:
|
|
57
|
+
decompilers['procyon'] = procyon_path
|
|
58
|
+
break
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# Check for javap (built-in with JDK)
|
|
63
|
+
try:
|
|
64
|
+
result = subprocess.run(['javap', '-help'],
|
|
65
|
+
capture_output=True, text=True, timeout=5)
|
|
66
|
+
if result.returncode == 0:
|
|
67
|
+
decompilers['javap'] = 'javap'
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
return decompilers
|
|
72
|
+
|
|
73
|
+
def decompile_class(self, jar_path: Path, class_name: str,
|
|
74
|
+
decompiler: Optional[str] = None) -> Optional[str]:
|
|
75
|
+
"""Decompile a specific class from a jar file"""
|
|
76
|
+
logger.info(f"Attempting to decompile {class_name} from {jar_path}")
|
|
77
|
+
|
|
78
|
+
if not self.available_decompilers:
|
|
79
|
+
logger.warning("No decompilers available, using fallback")
|
|
80
|
+
return self._fallback_class_info(jar_path, class_name)
|
|
81
|
+
|
|
82
|
+
# Choose decompiler
|
|
83
|
+
if not decompiler:
|
|
84
|
+
decompiler = next(iter(self.available_decompilers.keys()))
|
|
85
|
+
|
|
86
|
+
if decompiler not in self.available_decompilers:
|
|
87
|
+
logger.warning(f"Decompiler {decompiler} not available")
|
|
88
|
+
return self._fallback_class_info(jar_path, class_name)
|
|
89
|
+
|
|
90
|
+
logger.info(f"Using decompiler: {decompiler}")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
94
|
+
temp_path = Path(temp_dir)
|
|
95
|
+
|
|
96
|
+
# Extract class file
|
|
97
|
+
class_file_path = class_name.replace('.', '/') + '.class'
|
|
98
|
+
logger.debug(f"Looking for class file: {class_file_path}")
|
|
99
|
+
|
|
100
|
+
with zipfile.ZipFile(jar_path, 'r') as jar:
|
|
101
|
+
if class_file_path not in jar.namelist():
|
|
102
|
+
logger.error(f"Class file not found in jar: {class_file_path}")
|
|
103
|
+
return self._fallback_class_info(jar_path, class_name)
|
|
104
|
+
|
|
105
|
+
# Extract the class file
|
|
106
|
+
extracted_class = temp_path / 'extracted.class'
|
|
107
|
+
logger.debug(f"Extracting to: {extracted_class}")
|
|
108
|
+
with open(extracted_class, 'wb') as f:
|
|
109
|
+
f.write(jar.read(class_file_path))
|
|
110
|
+
|
|
111
|
+
# Decompile
|
|
112
|
+
result = self._run_decompiler(decompiler, extracted_class, temp_path)
|
|
113
|
+
if result:
|
|
114
|
+
logger.info(f"Decompilation successful with {decompiler}")
|
|
115
|
+
return result
|
|
116
|
+
else:
|
|
117
|
+
logger.warning(f"Decompilation failed with {decompiler}, trying fallback")
|
|
118
|
+
return self._fallback_class_info(jar_path, class_name)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Decompilation failed: {e}")
|
|
122
|
+
return self._fallback_class_info(jar_path, class_name)
|
|
123
|
+
|
|
124
|
+
def _run_decompiler(self, decompiler: str, class_file: Path, temp_dir: Path) -> Optional[str]:
|
|
125
|
+
"""Run the specified decompiler"""
|
|
126
|
+
logger.debug(f"Running decompiler {decompiler} on {class_file}")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if decompiler == 'javap':
|
|
130
|
+
# Use javap for disassembly
|
|
131
|
+
logger.debug("Using javap for disassembly")
|
|
132
|
+
result = subprocess.run([
|
|
133
|
+
'javap', '-v', '-p', '-c', str(class_file)
|
|
134
|
+
], capture_output=True, text=True, timeout=30)
|
|
135
|
+
|
|
136
|
+
logger.debug(f"javap return code: {result.returncode}")
|
|
137
|
+
if result.stderr:
|
|
138
|
+
logger.warning(f"javap stderr: {result.stderr}")
|
|
139
|
+
|
|
140
|
+
if result.returncode == 0:
|
|
141
|
+
return result.stdout
|
|
142
|
+
else:
|
|
143
|
+
logger.error(f"javap failed with return code {result.returncode}: {result.stderr}")
|
|
144
|
+
|
|
145
|
+
elif decompiler == 'cfr':
|
|
146
|
+
# Use CFR decompiler
|
|
147
|
+
output_dir = temp_dir / 'output'
|
|
148
|
+
output_dir.mkdir()
|
|
149
|
+
|
|
150
|
+
result = subprocess.run([
|
|
151
|
+
'java', '-jar', self.available_decompilers[decompiler],
|
|
152
|
+
str(class_file), '--outputdir', str(output_dir)
|
|
153
|
+
], capture_output=True, text=True, timeout=30)
|
|
154
|
+
|
|
155
|
+
# Look for generated .java file
|
|
156
|
+
java_files = list(output_dir.rglob('*.java'))
|
|
157
|
+
if java_files:
|
|
158
|
+
with open(java_files[0], 'r', encoding='utf-8') as f:
|
|
159
|
+
return f.read()
|
|
160
|
+
|
|
161
|
+
elif decompiler == 'procyon':
|
|
162
|
+
# Use Procyon decompiler
|
|
163
|
+
result = subprocess.run([
|
|
164
|
+
'java', '-jar', self.available_decompilers[decompiler],
|
|
165
|
+
str(class_file)
|
|
166
|
+
], capture_output=True, text=True, timeout=30)
|
|
167
|
+
|
|
168
|
+
if result.returncode == 0:
|
|
169
|
+
return result.stdout
|
|
170
|
+
|
|
171
|
+
elif decompiler == 'fernflower':
|
|
172
|
+
# Use Fernflower decompiler
|
|
173
|
+
output_dir = temp_dir / 'output'
|
|
174
|
+
output_dir.mkdir()
|
|
175
|
+
|
|
176
|
+
result = subprocess.run([
|
|
177
|
+
'java', '-jar', self.available_decompilers[decompiler],
|
|
178
|
+
str(class_file), str(output_dir)
|
|
179
|
+
], capture_output=True, text=True, timeout=30)
|
|
180
|
+
|
|
181
|
+
# Look for generated .java file
|
|
182
|
+
java_files = list(output_dir.rglob('*.java'))
|
|
183
|
+
if java_files:
|
|
184
|
+
with open(java_files[0], 'r', encoding='utf-8') as f:
|
|
185
|
+
return f.read()
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"Decompiler {decompiler} failed: {e}")
|
|
189
|
+
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def _fallback_class_info(self, jar_path: Path, class_name: str) -> str:
|
|
193
|
+
"""Fallback to basic class information when decompilation fails"""
|
|
194
|
+
try:
|
|
195
|
+
class_file_path = class_name.replace('.', '/') + '.class'
|
|
196
|
+
|
|
197
|
+
with zipfile.ZipFile(jar_path, 'r') as jar:
|
|
198
|
+
if class_file_path in jar.namelist():
|
|
199
|
+
class_data = jar.read(class_file_path)
|
|
200
|
+
|
|
201
|
+
# Basic bytecode analysis
|
|
202
|
+
info = f"// Decompilation not available\n"
|
|
203
|
+
info += f"// Class: {class_name}\n"
|
|
204
|
+
info += f"// Size: {len(class_data)} bytes\n"
|
|
205
|
+
info += f"// Location: {jar_path}\n\n"
|
|
206
|
+
|
|
207
|
+
# Try to extract some basic info from bytecode
|
|
208
|
+
magic = class_data[:4]
|
|
209
|
+
if magic == b'\xca\xfe\xba\xbe':
|
|
210
|
+
minor_version = int.from_bytes(class_data[4:6], 'big')
|
|
211
|
+
major_version = int.from_bytes(class_data[6:8], 'big')
|
|
212
|
+
info += f"// Java bytecode version: {major_version}.{minor_version}\n"
|
|
213
|
+
|
|
214
|
+
# Map major version to Java version
|
|
215
|
+
java_version = self._map_bytecode_version(major_version)
|
|
216
|
+
if java_version:
|
|
217
|
+
info += f"// Compiled for Java: {java_version}\n"
|
|
218
|
+
|
|
219
|
+
info += f"\npublic class {class_name.split('.')[-1]} {{\n"
|
|
220
|
+
info += " // Decompilation requires external tools\n"
|
|
221
|
+
info += " // Install CFR, Procyon, or Fernflower for full decompilation\n"
|
|
222
|
+
info += "}\n"
|
|
223
|
+
|
|
224
|
+
return info
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
return f"// Error reading class file: {e}"
|
|
228
|
+
|
|
229
|
+
return f"// Class not found: {class_name}"
|
|
230
|
+
|
|
231
|
+
def _map_bytecode_version(self, major_version: int) -> Optional[str]:
|
|
232
|
+
"""Map bytecode major version to Java version"""
|
|
233
|
+
version_map = {
|
|
234
|
+
45: "1.1",
|
|
235
|
+
46: "1.2",
|
|
236
|
+
47: "1.3",
|
|
237
|
+
48: "1.4",
|
|
238
|
+
49: "5",
|
|
239
|
+
50: "6",
|
|
240
|
+
51: "7",
|
|
241
|
+
52: "8",
|
|
242
|
+
53: "9",
|
|
243
|
+
54: "10",
|
|
244
|
+
55: "11",
|
|
245
|
+
56: "12",
|
|
246
|
+
57: "13",
|
|
247
|
+
58: "14",
|
|
248
|
+
59: "15",
|
|
249
|
+
60: "16",
|
|
250
|
+
61: "17",
|
|
251
|
+
62: "18",
|
|
252
|
+
63: "19",
|
|
253
|
+
64: "20",
|
|
254
|
+
65: "21"
|
|
255
|
+
}
|
|
256
|
+
return version_map.get(major_version)
|
|
257
|
+
|
|
258
|
+
def analyze_jar_structure(self, jar_path: Path) -> Dict[str, Any]:
|
|
259
|
+
"""Analyze the overall structure of a jar file"""
|
|
260
|
+
analysis = {
|
|
261
|
+
"jar_path": str(jar_path),
|
|
262
|
+
"total_size": jar_path.stat().st_size,
|
|
263
|
+
"packages": {},
|
|
264
|
+
"class_count": 0,
|
|
265
|
+
"resource_count": 0,
|
|
266
|
+
"manifest": {},
|
|
267
|
+
"services": [],
|
|
268
|
+
"annotations": []
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
with zipfile.ZipFile(jar_path, 'r') as jar:
|
|
273
|
+
entries = jar.namelist()
|
|
274
|
+
|
|
275
|
+
# Analyze entries
|
|
276
|
+
for entry in entries:
|
|
277
|
+
if entry.endswith('.class'):
|
|
278
|
+
analysis["class_count"] += 1
|
|
279
|
+
|
|
280
|
+
# Extract package info
|
|
281
|
+
if '/' in entry:
|
|
282
|
+
package = '/'.join(entry.split('/')[:-1]).replace('/', '.')
|
|
283
|
+
if package not in analysis["packages"]:
|
|
284
|
+
analysis["packages"][package] = 0
|
|
285
|
+
analysis["packages"][package] += 1
|
|
286
|
+
|
|
287
|
+
elif not entry.endswith('/'):
|
|
288
|
+
analysis["resource_count"] += 1
|
|
289
|
+
|
|
290
|
+
# Read manifest
|
|
291
|
+
if "META-INF/MANIFEST.MF" in entries:
|
|
292
|
+
manifest_content = jar.read("META-INF/MANIFEST.MF").decode('utf-8', errors='ignore')
|
|
293
|
+
analysis["manifest"] = self._parse_manifest(manifest_content)
|
|
294
|
+
|
|
295
|
+
# Look for services
|
|
296
|
+
service_entries = [e for e in entries if e.startswith("META-INF/services/")]
|
|
297
|
+
for service_entry in service_entries:
|
|
298
|
+
service_name = service_entry.replace("META-INF/services/", "")
|
|
299
|
+
try:
|
|
300
|
+
service_content = jar.read(service_entry).decode('utf-8', errors='ignore')
|
|
301
|
+
analysis["services"].append({
|
|
302
|
+
"interface": service_name,
|
|
303
|
+
"implementations": [line.strip() for line in service_content.split('\n') if line.strip()]
|
|
304
|
+
})
|
|
305
|
+
except Exception:
|
|
306
|
+
pass
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
analysis["error"] = str(e)
|
|
310
|
+
|
|
311
|
+
return analysis
|
|
312
|
+
|
|
313
|
+
def _parse_manifest(self, manifest_content: str) -> Dict[str, str]:
|
|
314
|
+
"""Parse JAR manifest file"""
|
|
315
|
+
manifest = {}
|
|
316
|
+
current_key = None
|
|
317
|
+
|
|
318
|
+
for line in manifest_content.split('\n'):
|
|
319
|
+
line = line.rstrip('\r')
|
|
320
|
+
|
|
321
|
+
if line.startswith(' ') and current_key:
|
|
322
|
+
# Continuation line
|
|
323
|
+
manifest[current_key] += line[1:]
|
|
324
|
+
elif ':' in line:
|
|
325
|
+
key, value = line.split(':', 1)
|
|
326
|
+
current_key = key.strip()
|
|
327
|
+
manifest[current_key] = value.strip()
|
|
328
|
+
|
|
329
|
+
return manifest
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decompiler paths configuration
|
|
3
|
+
Auto-generated by setup_decompilers.sh
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
DECOMPILER_PATHS = {
|
|
7
|
+
'cfr': 'decompilers/cfr.jar',
|
|
8
|
+
'procyon': 'decompilers/procyon-decompiler.jar',
|
|
9
|
+
# Uncomment and update path if you have Fernflower
|
|
10
|
+
# 'fernflower': 'path/to/java-decompiler.jar'
|
|
11
|
+
}
|