rea-agents 1.6.0 → 2.0.0

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.
Files changed (238) hide show
  1. package/README.md +157 -66
  2. package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
  3. package/bridge/hopper_bridge.py +13 -3
  4. package/dist/application/AbortablePromise.js +25 -0
  5. package/dist/application/AnalysisProvider.js +3 -0
  6. package/dist/application/AnalysisProviderEvaluation.js +60 -0
  7. package/dist/application/AnalysisProviderRegistry.js +228 -0
  8. package/dist/application/AnalysisSnapshotCache.js +41 -21
  9. package/dist/application/AnalysisSnapshotFiles.js +5 -6
  10. package/dist/application/BinarySession.js +218 -101
  11. package/dist/application/CapabilityInventory.js +18 -0
  12. package/dist/application/CompositeProvider.js +20 -8
  13. package/dist/application/CrossVersionInvestigation.js +10 -6
  14. package/dist/application/DirectAnalysis.js +49 -35
  15. package/dist/application/Doctor.js +110 -8
  16. package/dist/application/ElectronBoundaryAnalysis.js +96 -0
  17. package/dist/application/ElectronBoundaryGraph.js +9 -0
  18. package/dist/application/ElectronBoundaryGraphContext.js +62 -0
  19. package/dist/application/ElectronBoundaryGraphIpc.js +242 -0
  20. package/dist/application/ElectronBoundaryGraphNative.js +86 -0
  21. package/dist/application/ElectronBoundaryGraphWindows.js +199 -0
  22. package/dist/application/EnhancedTools.js +39 -14
  23. package/dist/application/InvestigationProviders.js +41 -0
  24. package/dist/application/JavaScriptApplicationEvidence.js +26 -0
  25. package/dist/application/JavaScriptApplicationEvidenceGraph.js +89 -0
  26. package/dist/application/JavaScriptApplicationService.js +74 -0
  27. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +29 -0
  28. package/dist/application/JavaScriptApplicationWorkflowService.js +90 -0
  29. package/dist/application/JavaScriptArtifactAnalysis.js +305 -0
  30. package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
  31. package/dist/application/JavaScriptArtifactFiles.js +231 -0
  32. package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
  33. package/dist/application/JavaScriptArtifactGraphBuilder.js +144 -0
  34. package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
  35. package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
  36. package/dist/application/JavaScriptArtifactGraphEvidence.js +99 -0
  37. package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
  38. package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
  39. package/dist/application/JavaScriptArtifactReconstruction.js +77 -0
  40. package/dist/application/JavaScriptArtifactReconstructionInput.js +18 -0
  41. package/dist/application/JavaScriptReplayPlanning.js +201 -0
  42. package/dist/application/JavaScriptReplayService.js +266 -0
  43. package/dist/application/JavaScriptRuntimeReconciliationEvidence.js +25 -0
  44. package/dist/application/JavaScriptRuntimeReconciliationService.js +27 -0
  45. package/dist/application/MacHopper.js +20 -4
  46. package/dist/application/ManagedApplicationGraphService.js +55 -0
  47. package/dist/application/ManagedMemberComparisonService.js +122 -0
  48. package/dist/application/ManagedNativeVerificationService.js +39 -0
  49. package/dist/application/ManagedReconstructionService.js +58 -0
  50. package/dist/application/ManagedRuntimeCorrelationService.js +103 -0
  51. package/dist/application/PermissionAuthority.js +1 -1
  52. package/dist/application/ProcessCaptureLifecycle.js +1 -1
  53. package/dist/application/ProcessHarness.js +1 -1
  54. package/dist/application/ProcessSampling.js +16 -12
  55. package/dist/application/ProjectPermissionStore.js +1 -0
  56. package/dist/application/SessionProviderRouter.js +219 -0
  57. package/dist/application/Setup.js +84 -60
  58. package/dist/application/SetupClients.js +1 -1
  59. package/dist/application/SetupPlan.js +9 -2
  60. package/dist/application/SupportedClients.js +40 -16
  61. package/dist/application/Upgrade.js +1 -1
  62. package/dist/application/runtime.js +11 -6
  63. package/dist/artifacts/AsarArtifactReader.js +23 -3
  64. package/dist/browser/CdpBrowserProvider.js +4 -2
  65. package/dist/browser/CdpCaptureEvents.js +49 -0
  66. package/dist/browser/CdpElectronInspection.js +106 -162
  67. package/dist/browser/CdpElectronProvider.js +9 -3
  68. package/dist/browser/CdpElectronScriptEvents.js +69 -0
  69. package/dist/browser/CdpElectronScripts.js +94 -0
  70. package/dist/browser/CdpElectronWorkers.js +56 -0
  71. package/dist/browser/CdpEndpoint.js +14 -1
  72. package/dist/browser/CdpPageCapture.js +8 -4
  73. package/dist/catalogIdentity.js +6 -42
  74. package/dist/cli.js +446 -34
  75. package/dist/cliApplicationCommands.js +62 -0
  76. package/dist/cliBrowserAdvancedCommands.js +5 -4
  77. package/dist/cliBrowserCommands.js +5 -4
  78. package/dist/cliCommandNames.js +56 -0
  79. package/dist/cliElectronCommands.js +106 -3
  80. package/dist/cliEvidenceCommands.js +4 -3
  81. package/dist/cliInvestigationCommands.js +2 -1
  82. package/dist/cliJsonInput.js +54 -0
  83. package/dist/cliPolicyCommands.js +2 -1
  84. package/dist/cliProcessCommands.js +3 -2
  85. package/dist/config.js +101 -2
  86. package/dist/contracts/applicationToolContracts.js +91 -0
  87. package/dist/contracts/artifactToolContracts.js +5 -9
  88. package/dist/contracts/browserToolContracts.js +14 -62
  89. package/dist/contracts/electronToolContracts.js +45 -19
  90. package/dist/contracts/enhancedInputs.js +2 -2
  91. package/dist/contracts/errorSchemas.js +1 -0
  92. package/dist/contracts/investigationExamples.js +0 -6
  93. package/dist/contracts/javascriptApplicationWorkflowExamples.js +46 -0
  94. package/dist/contracts/javascriptRuntimeReconciliationExample.js +208 -0
  95. package/dist/contracts/managedToolContracts.js +153 -0
  96. package/dist/contracts/managedWorkflowExamples.js +346 -0
  97. package/dist/contracts/managedWorkflowToolContracts.js +203 -0
  98. package/dist/contracts/nativeToolContracts.js +2 -6
  99. package/dist/contracts/promptContracts.js +18 -2
  100. package/dist/contracts/providerSelection.js +28 -0
  101. package/dist/contracts/sessionLifecycleInputs.js +2 -0
  102. package/dist/contracts/toolContractExamples.js +10 -25
  103. package/dist/contracts/toolContracts.js +69 -65
  104. package/dist/contracts/toolEffects.js +160 -0
  105. package/dist/contracts/toolOutputSchemas.js +152 -42
  106. package/dist/doctorRuntime.js +47 -0
  107. package/dist/domain/analysisProfile.js +78 -0
  108. package/dist/domain/analysisSnapshot.js +92 -46
  109. package/dist/domain/artifactComparison.js +3 -4
  110. package/dist/domain/artifactGraph.js +1 -0
  111. package/dist/domain/binaryTarget.js +4 -29
  112. package/dist/domain/browserObservation.js +3 -0
  113. package/dist/domain/bundleComparison.js +4 -4
  114. package/dist/domain/electronObservation.js +13 -0
  115. package/dist/domain/electronStaticAnalysis.js +9 -0
  116. package/dist/domain/electronStaticAnalysisBrowser.js +194 -0
  117. package/dist/domain/electronStaticAnalysisIpc.js +171 -0
  118. package/dist/domain/electronStaticAnalysisNative.js +164 -0
  119. package/dist/domain/electronStaticAnalysisTypes.js +1 -0
  120. package/dist/domain/electronStaticAnalysisValues.js +103 -0
  121. package/dist/domain/errors.js +148 -7
  122. package/dist/domain/evidence.js +44 -5
  123. package/dist/domain/evidenceBundle.js +9 -7
  124. package/dist/domain/functionComparison.js +9 -3
  125. package/dist/domain/functionComparisonNormalization.js +37 -0
  126. package/dist/domain/functionComparisonSchemas.js +3 -8
  127. package/dist/domain/functionDossierEvidence.js +4 -1
  128. package/dist/domain/hopperValues.js +66 -9
  129. package/dist/domain/inputIssueProjection.js +52 -0
  130. package/dist/domain/investigationWorkspace.js +2 -1
  131. package/dist/domain/javascriptApplicationAnalysis.js +155 -0
  132. package/dist/domain/javascriptApplicationChangeGraph.js +231 -0
  133. package/dist/domain/javascriptApplicationEvidenceSchemas.js +379 -0
  134. package/dist/domain/javascriptApplicationGraph.js +282 -0
  135. package/dist/domain/javascriptApplicationGraphSchemas.js +139 -0
  136. package/dist/domain/javascriptApplicationVersionComparison.js +116 -0
  137. package/dist/domain/javascriptApplicationVersionComparisonSchemas.js +139 -0
  138. package/dist/domain/javascriptApplicationVersionItems.js +221 -0
  139. package/dist/domain/javascriptApplicationVersionKeys.js +170 -0
  140. package/dist/domain/javascriptAstFingerprint.js +143 -0
  141. package/dist/domain/javascriptFeatureSeed.js +140 -0
  142. package/dist/domain/javascriptFeatureTrace.js +232 -0
  143. package/dist/domain/javascriptFeatureTraceSchemas.js +136 -0
  144. package/dist/domain/javascriptFeatureTraversal.js +88 -0
  145. package/dist/domain/javascriptNativeHandoff.js +85 -0
  146. package/dist/domain/javascriptReplay.js +361 -0
  147. package/dist/domain/javascriptRuntimeLoadState.js +106 -0
  148. package/dist/domain/javascriptRuntimeReconciliation.js +42 -0
  149. package/dist/domain/javascriptRuntimeReconciliationGraph.js +169 -0
  150. package/dist/domain/javascriptRuntimeReconciliationMatching.js +247 -0
  151. package/dist/domain/javascriptRuntimeReconciliationParsing.js +160 -0
  152. package/dist/domain/javascriptRuntimeReconciliationResult.js +119 -0
  153. package/dist/domain/javascriptRuntimeReconciliationRuntime.js +316 -0
  154. package/dist/domain/javascriptRuntimeReconciliationSchemas.js +245 -0
  155. package/dist/domain/javascriptRuntimeStaticCandidates.js +191 -0
  156. package/dist/domain/javascriptStaticAnalysis.js +346 -0
  157. package/dist/domain/javascriptStaticAnalysisFindings.js +39 -0
  158. package/dist/domain/javascriptStaticAnalysisHelpers.js +274 -0
  159. package/dist/domain/javascriptStaticAnalysisState.js +23 -0
  160. package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
  161. package/dist/domain/managedApplicationGraph.js +556 -0
  162. package/dist/domain/managedArtifact.js +444 -0
  163. package/dist/domain/managedMemberComparison.js +565 -0
  164. package/dist/domain/managedNativeVerification.js +408 -0
  165. package/dist/domain/managedReconstruction.js +183 -0
  166. package/dist/domain/managedRuntimeCorrelation.js +175 -0
  167. package/dist/domain/reconstructionVerificationSchemas.js +1 -3
  168. package/dist/dotnet/ManagedArtifactInspector.js +318 -0
  169. package/dist/dotnet/ManagedMemberInspector.js +887 -0
  170. package/dist/dotnet/ManagedMetadataHeaps.js +131 -0
  171. package/dist/dotnet/ManagedMetadataInventory.js +368 -0
  172. package/dist/dotnet/ManagedMetadataLayout.js +322 -0
  173. package/dist/dotnet/ManagedNativeBoundaryInspector.js +391 -0
  174. package/dist/dotnet/ManagedPeReader.js +157 -0
  175. package/dist/dotnet/ManagedReaderFailure.js +10 -0
  176. package/dist/dotnet/ManagedStaticProvider.js +203 -0
  177. package/dist/generatedPackageMetadata.js +2 -2
  178. package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
  179. package/dist/ghidra/GhidraClient.js +400 -0
  180. package/dist/ghidra/GhidraClientTypes.js +1 -0
  181. package/dist/ghidra/GhidraDefaults.js +20 -0
  182. package/dist/ghidra/GhidraDiagnostics.js +43 -0
  183. package/dist/ghidra/GhidraDoctor.js +59 -0
  184. package/dist/ghidra/GhidraFunctionValues.js +264 -0
  185. package/dist/ghidra/GhidraInstallation.js +239 -0
  186. package/dist/ghidra/GhidraInventoryValues.js +217 -0
  187. package/dist/ghidra/GhidraLauncher.js +139 -0
  188. package/dist/ghidra/GhidraProvider.js +366 -0
  189. package/dist/ghidra/GhidraRequestQueue.js +98 -0
  190. package/dist/ghidra/GhidraResponseBuffer.js +31 -0
  191. package/dist/ghidra/GhidraResponseRouter.js +24 -0
  192. package/dist/ghidra/GhidraSessionError.js +39 -0
  193. package/dist/ghidra/GhidraSessionValues.js +62 -0
  194. package/dist/ghidra/GhidraSocketConnection.js +46 -0
  195. package/dist/ghidra/protocol.js +69 -0
  196. package/dist/hopper/BridgeLauncher.js +19 -54
  197. package/dist/hopper/HopperAnalysisProfile.js +106 -0
  198. package/dist/hopper/HopperClient.js +154 -162
  199. package/dist/hopper/HopperProvider.js +99 -7
  200. package/dist/hopper/HopperSessionValues.js +27 -0
  201. package/dist/hopper/HopperSocketConnection.js +31 -0
  202. package/dist/main.js +14 -0
  203. package/dist/native/NativeMacOSProvider.js +3 -1
  204. package/dist/process/PendingOperations.js +59 -0
  205. package/dist/process/PrivateRuntimeRoot.js +33 -0
  206. package/dist/process/ProviderDeadline.js +95 -0
  207. package/dist/process/ProviderProcess.js +275 -0
  208. package/dist/replay/JavaScriptReplayWorker.js +286 -0
  209. package/dist/replay/LinuxJavaScriptReplayRunner.js +434 -0
  210. package/dist/replay/LinuxRuntimeClosure.js +46 -0
  211. package/dist/replay/LinuxSeccompPolicy.js +40 -0
  212. package/dist/replay/ReplayWorkerProtocol.js +55 -0
  213. package/dist/replay/SystemJavaScriptReplayHost.js +182 -0
  214. package/dist/server/createServer.js +53 -0
  215. package/dist/server/promptCompletion.js +9 -3
  216. package/dist/server/registerApplicationTools.js +115 -0
  217. package/dist/server/registerArtifactComparisonTool.js +16 -9
  218. package/dist/server/registerBrowserTools.js +33 -8
  219. package/dist/server/registerBundleComparisonTool.js +17 -20
  220. package/dist/server/registerElectronTools.js +45 -3
  221. package/dist/server/registerEnhancedTools.js +67 -19
  222. package/dist/server/registerEvidenceTools.js +5 -1
  223. package/dist/server/registerFunctionComparisonTool.js +14 -7
  224. package/dist/server/registerInvestigationTools.js +21 -17
  225. package/dist/server/registerManagedTools.js +6 -0
  226. package/dist/server/registerManagedWorkflowTools.js +277 -0
  227. package/dist/server/registerOfficialTools.js +11 -8
  228. package/dist/server/registerProcessComparisonTool.js +25 -47
  229. package/dist/server/registerSessionStatusTool.js +7 -7
  230. package/dist/server/registerSessionTools.js +57 -87
  231. package/dist/server/sessionEvidence.js +14 -19
  232. package/dist/server/toolInputValidation.js +77 -0
  233. package/dist/server/toolRegistrationOptions.js +79 -2
  234. package/dist/server/toolResult.js +25 -17
  235. package/package.json +17 -7
  236. package/scripts/rea.mjs +36 -2
  237. package/skills/rea-analysis/SKILL.md +59 -3
  238. /package/dist/{application → process}/ProcessOwnership.js +0 -0
@@ -0,0 +1,2075 @@
1
+ // REA's packaged, read-only Ghidra headless bridge.
2
+ // @category REA
3
+
4
+ import java.io.BufferedReader;
5
+ import java.io.BufferedWriter;
6
+ import java.io.ByteArrayOutputStream;
7
+ import java.io.IOException;
8
+ import java.io.Reader;
9
+ import java.math.BigInteger;
10
+ import java.net.StandardProtocolFamily;
11
+ import java.net.UnixDomainSocketAddress;
12
+ import java.nio.channels.Channels;
13
+ import java.nio.channels.ServerSocketChannel;
14
+ import java.nio.channels.SocketChannel;
15
+ import java.nio.charset.StandardCharsets;
16
+ import java.nio.file.Files;
17
+ import java.nio.file.LinkOption;
18
+ import java.nio.file.Path;
19
+ import java.nio.file.attribute.PosixFilePermissions;
20
+ import java.security.MessageDigest;
21
+ import java.util.ArrayList;
22
+ import java.util.Comparator;
23
+ import java.util.HashSet;
24
+ import java.util.List;
25
+ import java.util.Locale;
26
+ import java.util.Set;
27
+ import java.util.TreeMap;
28
+ import java.util.regex.Pattern;
29
+ import java.util.regex.PatternSyntaxException;
30
+
31
+ import com.google.gson.Gson;
32
+ import com.google.gson.GsonBuilder;
33
+ import com.google.gson.JsonArray;
34
+ import com.google.gson.JsonElement;
35
+ import com.google.gson.JsonNull;
36
+ import com.google.gson.JsonObject;
37
+ import com.google.gson.JsonParser;
38
+
39
+ import ghidra.app.util.headless.HeadlessScript;
40
+ import ghidra.app.decompiler.DecompInterface;
41
+ import ghidra.app.decompiler.DecompileOptions;
42
+ import ghidra.app.decompiler.DecompileResults;
43
+ import ghidra.app.decompiler.DecompiledFunction;
44
+ import ghidra.framework.Application;
45
+ import ghidra.program.model.address.Address;
46
+ import ghidra.program.model.address.AddressSpace;
47
+ import ghidra.program.model.block.BasicBlockModel;
48
+ import ghidra.program.model.block.CodeBlock;
49
+ import ghidra.program.model.block.CodeBlockIterator;
50
+ import ghidra.program.model.block.CodeBlockReference;
51
+ import ghidra.program.model.block.CodeBlockReferenceIterator;
52
+ import ghidra.program.model.data.StringDataInstance;
53
+ import ghidra.program.model.listing.CommentType;
54
+ import ghidra.program.model.listing.Data;
55
+ import ghidra.program.model.listing.DataIterator;
56
+ import ghidra.program.model.listing.Function;
57
+ import ghidra.program.model.listing.FunctionIterator;
58
+ import ghidra.program.model.listing.Instruction;
59
+ import ghidra.program.model.listing.InstructionIterator;
60
+ import ghidra.program.model.listing.Variable;
61
+ import ghidra.program.model.mem.MemoryBlock;
62
+ import ghidra.program.model.symbol.RefType;
63
+ import ghidra.program.model.symbol.Reference;
64
+ import ghidra.program.model.symbol.ReferenceIterator;
65
+ import ghidra.program.model.symbol.SourceType;
66
+ import ghidra.program.model.symbol.Symbol;
67
+ import ghidra.program.model.symbol.SymbolIterator;
68
+
69
+ public final class ReaGhidraBridge extends HeadlessScript {
70
+ private static final int BRIDGE_VERSION = 3;
71
+ private static final int MAX_DESCRIPTOR_BYTES = 16 * 1024;
72
+ private static final int MAX_REQUEST_CHARACTERS = 256 * 1024;
73
+ private static final int MAX_RESPONSE_BYTES = 1024 * 1024;
74
+ private static final int MAX_INVENTORY_ITEMS = 1_000_000;
75
+ private static final int MAX_FUNCTION_ITEMS = 100_000;
76
+ private static final int MAX_FUNCTION_INSTRUCTIONS = 100_000;
77
+ private static final int DECOMPILE_TIMEOUT_SECONDS = 30;
78
+ private static final int DECOMPILE_PAYLOAD_MBYTES = 8;
79
+ private static final int MAX_LIST_VALUE_CODE_POINTS = 1_024;
80
+ private static final int MAX_SEARCH_VALUE_CODE_POINTS = 4_096;
81
+ private static final int MAX_REGEX_CANDIDATE_CHARACTERS = 4_096;
82
+ private static final long MAX_REGEX_BACKTRACKING_PATHS = 10_000;
83
+ private static final long MAX_LITERAL_SEARCH_WORK_UNITS = 1_000_000;
84
+ private static final long MAX_REGEX_SEARCH_WORK_UNITS = 1_000_000;
85
+ private static final Gson GSON = new GsonBuilder().serializeNulls().create();
86
+ private static final Set<String> DESCRIPTOR_KEYS = Set.of(
87
+ "schema_version",
88
+ "socket_path",
89
+ "token",
90
+ "run_id",
91
+ "provider_version",
92
+ "profile_digest"
93
+ );
94
+ private static final Set<String> REQUEST_KEYS = Set.of(
95
+ "id",
96
+ "token",
97
+ "method",
98
+ "params"
99
+ );
100
+ private static final String[] CAPABILITIES = {
101
+ "ping",
102
+ "shutdown",
103
+ "address_name",
104
+ "list_documents",
105
+ "list_names",
106
+ "list_procedures",
107
+ "list_segments",
108
+ "list_strings",
109
+ "procedure_address",
110
+ "resolve_containing_procedure",
111
+ "search_procedures",
112
+ "search_strings",
113
+ "analyze_function",
114
+ "procedure_assembly",
115
+ "procedure_callees",
116
+ "procedure_callers",
117
+ "procedure_info",
118
+ "procedure_pseudo_code",
119
+ "procedure_references",
120
+ "xrefs"
121
+ };
122
+
123
+ private List<InventoryItem> nameInventory;
124
+ private List<FunctionEntry> procedureInventory;
125
+ private List<InventoryItem> stringInventory;
126
+ private DecompInterface decompiler;
127
+ private static AddressSpace sessionDefaultAddressSpace;
128
+
129
+ @Override
130
+ public void run() throws Exception {
131
+ String[] arguments = getScriptArgs();
132
+ if (arguments.length != 1) {
133
+ throw new IllegalArgumentException("REA bridge requires one session descriptor");
134
+ }
135
+ Path descriptorPath = Path.of(arguments[0]);
136
+ SessionDescriptor descriptor = readDescriptor(descriptorPath);
137
+ Files.deleteIfExists(descriptorPath);
138
+ if (currentProgram == null) {
139
+ throw new IllegalStateException("REA bridge requires an imported program");
140
+ }
141
+ sessionDefaultAddressSpace =
142
+ currentProgram.getAddressFactory().getDefaultAddressSpace();
143
+ if (!Application.getApplicationVersion().equals(descriptor.providerVersion)) {
144
+ throw new IllegalStateException("Ghidra provider version does not match the session");
145
+ }
146
+ try {
147
+ initializeDecompiler();
148
+ serve(descriptor);
149
+ }
150
+ finally {
151
+ if (decompiler != null) {
152
+ decompiler.dispose();
153
+ decompiler = null;
154
+ }
155
+ }
156
+ }
157
+
158
+ private void serve(SessionDescriptor descriptor) throws Exception {
159
+ Path socketPath = Path.of(descriptor.socketPath);
160
+ Files.deleteIfExists(socketPath);
161
+ try (ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {
162
+ server.bind(UnixDomainSocketAddress.of(socketPath));
163
+ Files.setPosixFilePermissions(socketPath, PosixFilePermissions.fromString("rw-------"));
164
+ try (SocketChannel client = server.accept();
165
+ BufferedReader reader = new BufferedReader(Channels.newReader(client, StandardCharsets.UTF_8));
166
+ BufferedWriter writer = new BufferedWriter(Channels.newWriter(client, StandardCharsets.UTF_8))) {
167
+ serveClient(descriptor, reader, writer);
168
+ }
169
+ }
170
+ finally {
171
+ Files.deleteIfExists(socketPath);
172
+ }
173
+ }
174
+
175
+ private void serveClient(
176
+ SessionDescriptor descriptor,
177
+ BufferedReader reader,
178
+ BufferedWriter writer) throws Exception {
179
+ while (true) {
180
+ String line = readBoundedLine(reader, MAX_REQUEST_CHARACTERS);
181
+ if (line == null) {
182
+ return;
183
+ }
184
+ Request request;
185
+ try {
186
+ request = parseRequest(line, descriptor.token);
187
+ }
188
+ catch (RuntimeException exception) {
189
+ writeFailure(writer, 1, "invalid_request", "Bridge request is invalid");
190
+ return;
191
+ }
192
+ try {
193
+ if (request.method.equals("shutdown")) {
194
+ requireKeys(request.params, Set.of());
195
+ JsonObject result = new JsonObject();
196
+ result.addProperty("shutdown", true);
197
+ result.addProperty("project_ephemeral", true);
198
+ writeSuccess(writer, request.id, result);
199
+ return;
200
+ }
201
+ writeSuccess(writer, request.id, handleRequest(request, descriptor));
202
+ }
203
+ catch (RequestFailure failure) {
204
+ writeFailure(writer, request.id, failure.code, failure.getMessage());
205
+ }
206
+ catch (Exception exception) {
207
+ writeFailure(
208
+ writer,
209
+ request.id,
210
+ "bridge_exception",
211
+ exception.getClass().getSimpleName() + ": " + safeMessage(exception)
212
+ );
213
+ }
214
+ }
215
+ }
216
+
217
+ private JsonElement handleRequest(Request request, SessionDescriptor descriptor)
218
+ throws Exception {
219
+ if (request.method.equals("ping")) {
220
+ requireKeys(request.params, Set.of());
221
+ return sessionInfo(descriptor);
222
+ }
223
+ if (analysisTimeoutOccurred()) {
224
+ throw new RequestFailure(
225
+ "analysis_incomplete",
226
+ "Ghidra auto-analysis did not complete for this Program"
227
+ );
228
+ }
229
+ return switch (request.method) {
230
+ case "address_name" -> addressName(request.params);
231
+ case "list_documents" -> listDocuments(request.params);
232
+ case "list_names" -> listNames(request.params);
233
+ case "list_procedures" -> listProcedures(request.params);
234
+ case "list_segments" -> listSegments(request.params);
235
+ case "list_strings" -> listStrings(request.params);
236
+ case "procedure_address" -> procedureAddress(request.params);
237
+ case "procedure_assembly" -> procedureAssembly(request.params);
238
+ case "procedure_callees" -> procedureCalls(request.params, false);
239
+ case "procedure_callers" -> procedureCalls(request.params, true);
240
+ case "procedure_info" -> procedureInfo(request.params);
241
+ case "procedure_pseudo_code" -> procedurePseudocode(request.params);
242
+ case "procedure_references" -> procedureReferences(request.params);
243
+ case "resolve_containing_procedure" -> containingProcedure(request.params);
244
+ case "search_procedures" -> search(request.params, true);
245
+ case "search_strings" -> search(request.params, false);
246
+ case "xrefs" -> xrefs(request.params);
247
+ case "analyze_function" -> analyzeFunction(request.params);
248
+ default -> throw new RequestFailure(
249
+ "method_unavailable",
250
+ "Bridge method is unavailable"
251
+ );
252
+ };
253
+ }
254
+
255
+ private JsonObject sessionInfo(SessionDescriptor descriptor) throws Exception {
256
+ boolean timedOut = analysisTimeoutOccurred();
257
+ JsonObject provider = new JsonObject();
258
+ provider.addProperty("id", "ghidra");
259
+ provider.addProperty("version", Application.getApplicationVersion());
260
+
261
+ JsonObject target = new JsonObject();
262
+ target.addProperty("name", currentProgram.getName());
263
+ target.addProperty("language_id", currentProgram.getLanguageID().getIdAsString());
264
+ target.addProperty(
265
+ "compiler_spec_id",
266
+ currentProgram.getCompilerSpec().getCompilerSpecID().getIdAsString()
267
+ );
268
+ target.addProperty("image_base", canonicalAddress(currentProgram.getImageBase()));
269
+ target.addProperty(
270
+ "default_address_space",
271
+ currentProgram.getAddressFactory().getDefaultAddressSpace().getName()
272
+ );
273
+
274
+ JsonObject result = new JsonObject();
275
+ result.addProperty("name", "REA Ghidra bridge");
276
+ result.addProperty("bridge_version", BRIDGE_VERSION);
277
+ result.addProperty("run_id", descriptor.runId);
278
+ result.addProperty("profile_digest", descriptor.profileDigest);
279
+ result.add("provider", provider);
280
+ result.addProperty("read_only", true);
281
+ result.addProperty("analysis_complete", !timedOut);
282
+ result.addProperty("analysis_timed_out", timedOut);
283
+ result.add("capabilities", GSON.toJsonTree(CAPABILITIES));
284
+ result.add("target", target);
285
+ return result;
286
+ }
287
+
288
+ private void initializeDecompiler() {
289
+ DecompileOptions options = new DecompileOptions();
290
+ options.setDefaultTimeout(DECOMPILE_TIMEOUT_SECONDS);
291
+ options.setMaxPayloadMBytes(DECOMPILE_PAYLOAD_MBYTES);
292
+ options.setMaxInstructions(MAX_FUNCTION_INSTRUCTIONS);
293
+ decompiler = new DecompInterface();
294
+ decompiler.setOptions(options);
295
+ decompiler.toggleCCode(true);
296
+ decompiler.toggleSyntaxTree(true);
297
+ if (!decompiler.openProgram(currentProgram)) {
298
+ throw new IllegalStateException("Ghidra decompiler could not open the imported Program");
299
+ }
300
+ }
301
+
302
+ private JsonElement procedurePseudocode(JsonObject params) throws Exception {
303
+ requireKeys(params, Set.of("document", "procedure"));
304
+ requireDocument(params);
305
+ String value = decompile(resolveProcedure(requireString(params, "procedure")));
306
+ return value == null ? JsonNull.INSTANCE : GSON.toJsonTree(value);
307
+ }
308
+
309
+ private JsonElement procedureAssembly(JsonObject params) throws Exception {
310
+ requireKeys(params, Set.of("document", "procedure"));
311
+ requireDocument(params);
312
+ Function function = resolveProcedure(requireString(params, "procedure"));
313
+ InstructionScan scan = scanInstructions(function, MAX_FUNCTION_INSTRUCTIONS);
314
+ if (scan.truncated) {
315
+ throw functionLimit("Procedure assembly exceeds the 100000-instruction limit");
316
+ }
317
+ return GSON.toJsonTree(renderAssembly(scan.instructions));
318
+ }
319
+
320
+ private JsonArray procedureCalls(JsonObject params, boolean callers) throws Exception {
321
+ requireKeys(params, Set.of("document", "procedure"));
322
+ requireDocument(params);
323
+ Function function = resolveProcedure(requireString(params, "procedure"));
324
+ Set<Function> observed = callers
325
+ ? function.getCallingFunctions(monitor)
326
+ : function.getCalledFunctions(monitor);
327
+ if (observed.size() > MAX_FUNCTION_ITEMS) {
328
+ throw functionLimit("Resolved call set exceeds the 100000-item limit");
329
+ }
330
+ List<Function> ordered = new ArrayList<>(observed);
331
+ ordered.sort(Comparator.comparing(Function::getEntryPoint));
332
+ JsonArray result = new JsonArray();
333
+ for (Function item : ordered) {
334
+ result.add(canonicalAddress(item.getEntryPoint()));
335
+ }
336
+ return result;
337
+ }
338
+
339
+ private JsonObject procedureInfo(JsonObject params) throws Exception {
340
+ requireKeys(params, Set.of("document", "procedure"));
341
+ requireDocument(params);
342
+ Function function = resolveProcedure(requireString(params, "procedure"));
343
+ JsonObject result = new JsonObject();
344
+ result.addProperty("name", procedureName(function));
345
+ result.addProperty("entrypoint", canonicalAddress(function.getEntryPoint()));
346
+ result.addProperty("basicblock_count", basicBlocks(function).size());
347
+ result.addProperty("length", function.getBody().getNumAddresses());
348
+ result.addProperty("signature", function.getPrototypeString(false, true));
349
+ result.add("locals", functionLocals(function));
350
+ result.add("classification", functionClassification(function));
351
+ return result;
352
+ }
353
+
354
+ private JsonObject procedureReferences(JsonObject params) throws Exception {
355
+ requireKeys(
356
+ params,
357
+ Set.of(
358
+ "document",
359
+ "procedure",
360
+ "direction",
361
+ "offset",
362
+ "limit",
363
+ "max_instructions"
364
+ )
365
+ );
366
+ requireDocument(params);
367
+ Function function = resolveProcedure(requireString(params, "procedure"));
368
+ String direction = requireString(params, "direction");
369
+ if (!direction.equals("incoming") && !direction.equals("outgoing")) {
370
+ throw new RequestFailure("invalid_request", "direction must be incoming or outgoing");
371
+ }
372
+ int offset = requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE);
373
+ int limit = requireBoundedInteger(params, "limit", 1, 500);
374
+ int maximum = requireBoundedInteger(params, "max_instructions", 1, 5_000);
375
+ InstructionScan scan = scanInstructions(function, maximum);
376
+ List<Reference> references = collectReferences(scan.instructions, direction);
377
+ JsonArray edges = new JsonArray();
378
+ for (Reference reference : references) {
379
+ edges.add(referenceEdge(reference));
380
+ }
381
+ JsonObject result = new JsonObject();
382
+ result.add("procedure", procedureIdentity(function));
383
+ result.addProperty("direction", direction);
384
+ result.add("references", bounded(edges, offset, limit, !scan.truncated));
385
+ result.addProperty("instructions_scanned", scan.instructions.size());
386
+ result.addProperty("instruction_scan_truncated", scan.truncated);
387
+ return result;
388
+ }
389
+
390
+ private JsonArray xrefs(JsonObject params) throws Exception {
391
+ requireKeys(params, Set.of("document", "address"));
392
+ requireDocument(params);
393
+ Address address = requireAddress(params, "address");
394
+ ReferenceIterator references = currentProgram.getReferenceManager().getReferencesTo(address);
395
+ Set<Address> sources = new HashSet<>();
396
+ while (references.hasNext()) {
397
+ monitor.checkCancelled();
398
+ Reference reference = references.next();
399
+ if (reference.isEntryPointReference()) {
400
+ continue;
401
+ }
402
+ sources.add(reference.getFromAddress());
403
+ if (sources.size() > MAX_FUNCTION_ITEMS) {
404
+ throw functionLimit("Cross-reference set exceeds the 100000-item limit");
405
+ }
406
+ }
407
+ List<Address> ordered = new ArrayList<>(sources);
408
+ ordered.sort(Address::compareTo);
409
+ JsonArray result = new JsonArray();
410
+ for (Address source : ordered) {
411
+ result.add(canonicalAddress(source));
412
+ }
413
+ return result;
414
+ }
415
+
416
+ private JsonObject analyzeFunction(JsonObject params) throws Exception {
417
+ requireKeys(
418
+ params,
419
+ Set.of(
420
+ "procedure",
421
+ "include_assembly",
422
+ "limit",
423
+ "max_pseudocode_chars",
424
+ "max_instructions",
425
+ "pseudocode_offset",
426
+ "assembly_offset",
427
+ "collection_offset"
428
+ )
429
+ );
430
+ Function function = resolveProcedure(requireString(params, "procedure"));
431
+ boolean includeAssembly = requireBoolean(params, "include_assembly");
432
+ int limit = requireBoundedInteger(params, "limit", 1, 500);
433
+ int maximumCharacters = requireBoundedInteger(
434
+ params,
435
+ "max_pseudocode_chars",
436
+ 1,
437
+ 100_000
438
+ );
439
+ int maximumInstructions = requireBoundedInteger(
440
+ params,
441
+ "max_instructions",
442
+ 1,
443
+ 5_000
444
+ );
445
+ int pseudocodeOffset = requireBoundedInteger(
446
+ params,
447
+ "pseudocode_offset",
448
+ 0,
449
+ Integer.MAX_VALUE
450
+ );
451
+ int assemblyOffset = requireBoundedInteger(
452
+ params,
453
+ "assembly_offset",
454
+ 0,
455
+ Integer.MAX_VALUE
456
+ );
457
+ JsonObject offsets = requireCollectionOffsets(params);
458
+ InstructionScan scan = scanInstructions(function, maximumInstructions);
459
+ String pseudocode = decompile(function);
460
+ if (pseudocode == null) {
461
+ pseudocode = "";
462
+ }
463
+ List<Reference> incomingReferences = collectReferences(scan.instructions, "incoming")
464
+ .stream()
465
+ .filter(reference -> !function.getBody().contains(reference.getFromAddress()))
466
+ .toList();
467
+ List<Reference> outgoingReferences = collectReferences(scan.instructions, "outgoing");
468
+ JsonArray incoming = referenceEdges(incomingReferences);
469
+ JsonArray outgoing = referenceEdges(outgoingReferences);
470
+ JsonArray comments = comments(scan.instructions);
471
+ JsonArray callers = procedureIdentities(function.getCallingFunctions(monitor));
472
+ JsonArray callees = procedureIdentities(function.getCalledFunctions(monitor));
473
+ JsonArray referencedStrings = referencedStrings(outgoingReferences);
474
+ JsonArray referencedNames = referencedNames(outgoingReferences);
475
+ JsonArray blocks = basicBlockValues(function);
476
+ JsonArray assembly = includeAssembly
477
+ ? GSON.toJsonTree(renderAssemblyLines(scan.instructions)).getAsJsonArray()
478
+ : new JsonArray();
479
+
480
+ JsonObject procedure = procedureIdentity(function);
481
+ procedure.addProperty("signature", function.getPrototypeString(false, true));
482
+ procedure.add("locals", functionLocals(function));
483
+ JsonObject result = new JsonObject();
484
+ result.add("procedure", procedure);
485
+ result.add(
486
+ "pseudocode",
487
+ pseudocodePage(pseudocode, pseudocodeOffset, maximumCharacters)
488
+ );
489
+ result.add(
490
+ "assembly",
491
+ includeAssembly
492
+ ? bounded(assembly, assemblyOffset, maximumInstructions, !scan.truncated)
493
+ : bounded(assembly, 0, maximumInstructions, true)
494
+ );
495
+ result.add(
496
+ "comments",
497
+ bounded(comments, collectionOffset(offsets, "comments"), limit, !scan.truncated)
498
+ );
499
+ result.add("callers", bounded(callers, collectionOffset(offsets, "callers"), limit, true));
500
+ result.add("callees", bounded(callees, collectionOffset(offsets, "callees"), limit, true));
501
+ result.add(
502
+ "incoming_references",
503
+ bounded(
504
+ incoming,
505
+ collectionOffset(offsets, "incoming_references"),
506
+ limit,
507
+ !scan.truncated
508
+ )
509
+ );
510
+ result.add(
511
+ "outgoing_references",
512
+ bounded(
513
+ outgoing,
514
+ collectionOffset(offsets, "outgoing_references"),
515
+ limit,
516
+ !scan.truncated
517
+ )
518
+ );
519
+ result.add(
520
+ "referenced_strings",
521
+ bounded(
522
+ referencedStrings,
523
+ collectionOffset(offsets, "referenced_strings"),
524
+ limit,
525
+ !scan.truncated
526
+ )
527
+ );
528
+ result.add(
529
+ "referenced_names",
530
+ bounded(
531
+ referencedNames,
532
+ collectionOffset(offsets, "referenced_names"),
533
+ limit,
534
+ !scan.truncated
535
+ )
536
+ );
537
+ result.add(
538
+ "basic_blocks",
539
+ bounded(blocks, collectionOffset(offsets, "basic_blocks"), limit, true)
540
+ );
541
+ JsonObject instructionScan = new JsonObject();
542
+ instructionScan.addProperty("scanned", scan.instructions.size());
543
+ instructionScan.addProperty("truncated", scan.truncated);
544
+ result.add("instruction_scan", instructionScan);
545
+ JsonArray limitations = new JsonArray();
546
+ limitations.add(
547
+ "Unresolved computed or indirect flows without target addresses are not represented as reference edges."
548
+ );
549
+ limitations.add(
550
+ "Thunk and external classifications are Ghidra FunctionManager observations; they do not resolve targetless calls."
551
+ );
552
+ limitations.add(
553
+ "Pseudocode and assembly are Ghidra-specific representations, not original source or Hopper-equivalent text."
554
+ );
555
+ limitations.add(
556
+ "Synthetic Ghidra entry-point references without actionable memory sources are omitted."
557
+ );
558
+ result.add("limitations", limitations);
559
+ return result;
560
+ }
561
+
562
+ private JsonElement addressName(JsonObject params) {
563
+ requireKeys(params, Set.of("document", "address"));
564
+ requireDocument(params);
565
+ Address address = requireAddress(params, "address");
566
+ Symbol symbol = currentProgram.getSymbolTable().getPrimarySymbol(address);
567
+ return symbol == null ? JsonNull.INSTANCE : GSON.toJsonTree(symbol.getName(true));
568
+ }
569
+
570
+ private JsonArray listDocuments(JsonObject params) {
571
+ requireKeys(params, Set.of());
572
+ JsonArray result = new JsonArray();
573
+ result.add(currentProgram.getName());
574
+ return result;
575
+ }
576
+
577
+ private JsonObject listNames(JsonObject params) throws Exception {
578
+ requireKeys(params, Set.of("document", "address", "offset", "limit"));
579
+ requireDocument(params);
580
+ List<InventoryItem> inventory = names();
581
+ String requested = optionalString(params, "address");
582
+ if (requested != null) {
583
+ Address address = parseReaAddress(requested);
584
+ inventory = inventory.stream()
585
+ .filter(item -> item.address.equals(address))
586
+ .toList();
587
+ }
588
+ return page(
589
+ inventory,
590
+ requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE),
591
+ requireBoundedInteger(params, "limit", 1, 500),
592
+ "symbol"
593
+ );
594
+ }
595
+
596
+ private JsonObject listProcedures(JsonObject params) throws Exception {
597
+ requireKeys(params, Set.of("document", "offset", "limit"));
598
+ requireDocument(params);
599
+ List<InventoryItem> inventory = procedures().stream()
600
+ .map(FunctionEntry::item)
601
+ .toList();
602
+ return page(
603
+ inventory,
604
+ requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE),
605
+ requireBoundedInteger(params, "limit", 1, 500),
606
+ "procedure"
607
+ );
608
+ }
609
+
610
+ private JsonArray listSegments(JsonObject params) {
611
+ requireKeys(params, Set.of("document"));
612
+ requireDocument(params);
613
+ JsonArray result = new JsonArray();
614
+ String imageBase = canonicalAddress(currentProgram.getImageBase());
615
+ MemoryBlock[] blocks = currentProgram.getMemory().getBlocks();
616
+ if (blocks.length > MAX_INVENTORY_ITEMS) {
617
+ throw inventoryLimit();
618
+ }
619
+ List<MemoryBlock> ordered = new ArrayList<>(List.of(blocks));
620
+ ordered.sort(Comparator.comparing(MemoryBlock::getStart));
621
+ for (MemoryBlock block : ordered) {
622
+ JsonObject item = memoryRegion(block, imageBase);
623
+ item.add("sections", new JsonArray());
624
+ result.add(item);
625
+ }
626
+ return result;
627
+ }
628
+
629
+ private JsonObject listStrings(JsonObject params) throws Exception {
630
+ requireKeys(params, Set.of("document", "address", "offset", "limit"));
631
+ requireDocument(params);
632
+ List<InventoryItem> inventory = strings();
633
+ String requested = optionalString(params, "address");
634
+ if (requested != null) {
635
+ Address address = parseReaAddress(requested);
636
+ inventory = inventory.stream()
637
+ .filter(item -> item.address.equals(address))
638
+ .toList();
639
+ }
640
+ return page(
641
+ inventory,
642
+ requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE),
643
+ requireBoundedInteger(params, "limit", 1, 500),
644
+ "string"
645
+ );
646
+ }
647
+
648
+ private JsonElement procedureAddress(JsonObject params) throws Exception {
649
+ requireKeys(params, Set.of("document", "procedure"));
650
+ requireDocument(params);
651
+ return GSON.toJsonTree(canonicalAddress(resolveProcedure(requireString(params, "procedure")).getEntryPoint()));
652
+ }
653
+
654
+ private JsonObject containingProcedure(JsonObject params) {
655
+ requireKeys(params, Set.of("document", "address"));
656
+ requireDocument(params);
657
+ Address query = requireAddress(params, "address");
658
+ Function function = currentProgram.getFunctionManager().getFunctionAt(query);
659
+ if (function == null && query.isMemoryAddress()) {
660
+ function = currentProgram.getFunctionManager().getFunctionContaining(query);
661
+ }
662
+ JsonObject result = new JsonObject();
663
+ result.addProperty("query_address", canonicalAddress(query));
664
+ if (function != null) {
665
+ result.addProperty("found", true);
666
+ result.add("procedure", procedureIdentity(function));
667
+ return result;
668
+ }
669
+ result.addProperty("found", false);
670
+ result.add("procedure", JsonNull.INSTANCE);
671
+ boolean inside = query.isMemoryAddress() && currentProgram.getMemory().contains(query);
672
+ result.addProperty("reason", inside ? "not_in_procedure" : "outside_segments");
673
+ return result;
674
+ }
675
+
676
+ private JsonObject search(JsonObject params, boolean procedureSearch) throws Exception {
677
+ requireKeys(
678
+ params,
679
+ Set.of("pattern", "mode", "case_sensitive", "offset", "limit", "document")
680
+ );
681
+ requireDocument(params);
682
+ String expression = requireString(params, "pattern");
683
+ if (expression.codePointCount(0, expression.length()) > 256) {
684
+ throw new RequestFailure(
685
+ "invalid_request",
686
+ "pattern must contain between 1 and 256 characters"
687
+ );
688
+ }
689
+ String mode = requireString(params, "mode");
690
+ if (!mode.equals("literal") && !mode.equals("regex")) {
691
+ throw new RequestFailure("invalid_request", "mode must be literal or regex");
692
+ }
693
+ boolean caseSensitive = requireBoolean(params, "case_sensitive");
694
+ int offset = requireBoundedInteger(params, "offset", 0, Integer.MAX_VALUE);
695
+ int limit = requireBoundedInteger(params, "limit", 1, 100);
696
+ List<InventoryItem> inventory = procedureSearch
697
+ ? procedures().stream().map(FunctionEntry::item).toList()
698
+ : strings();
699
+ ValueMatcher matcher = mode.equals("literal")
700
+ ? literalMatcher(expression, caseSensitive)
701
+ : regexMatcher(expression, caseSensitive);
702
+ JsonArray items = new JsonArray();
703
+ int total = 0;
704
+ long pageEnd = (long) offset + limit;
705
+ for (InventoryItem item : inventory) {
706
+ if (!matcher.matches(item.value)) {
707
+ continue;
708
+ }
709
+ if (total >= offset && total < pageEnd) {
710
+ items.add(resultItem(item, null, MAX_SEARCH_VALUE_CODE_POINTS));
711
+ }
712
+ total += 1;
713
+ }
714
+ return pageResult(items, offset, limit, total);
715
+ }
716
+
717
+ private List<InventoryItem> names() throws Exception {
718
+ if (nameInventory != null) {
719
+ return nameInventory;
720
+ }
721
+ List<InventoryItem> result = new ArrayList<>();
722
+ SymbolIterator symbols = currentProgram.getSymbolTable().getAllSymbols(true);
723
+ while (symbols.hasNext()) {
724
+ monitor.checkCancelled();
725
+ Symbol symbol = symbols.next();
726
+ Address address = symbol.getAddress();
727
+ if (!address.isMemoryAddress() && !address.isExternalAddress()) {
728
+ continue;
729
+ }
730
+ JsonObject facts = new JsonObject();
731
+ facts.addProperty("primary", symbol.isPrimary());
732
+ facts.addProperty("dynamic", symbol.isDynamic());
733
+ facts.addProperty("external", symbol.isExternal());
734
+ facts.addProperty("type", normalizedSymbolType(symbol));
735
+ facts.addProperty("source", normalizedSource(symbol.getSource()));
736
+ addBounded(
737
+ result,
738
+ new InventoryItem(address, symbol.getName(true), facts)
739
+ );
740
+ }
741
+ result.sort(INVENTORY_ORDER);
742
+ nameInventory = List.copyOf(result);
743
+ return nameInventory;
744
+ }
745
+
746
+ private List<FunctionEntry> procedures() throws Exception {
747
+ if (procedureInventory != null) {
748
+ return procedureInventory;
749
+ }
750
+ List<FunctionEntry> result = new ArrayList<>();
751
+ Set<Address> seen = new HashSet<>();
752
+ appendFunctions(result, seen, currentProgram.getFunctionManager().getFunctions(true));
753
+ appendFunctions(result, seen, currentProgram.getFunctionManager().getExternalFunctions());
754
+ result.sort((left, right) -> INVENTORY_ORDER.compare(left.item, right.item));
755
+ procedureInventory = List.copyOf(result);
756
+ return procedureInventory;
757
+ }
758
+
759
+ private void appendFunctions(
760
+ List<FunctionEntry> destination,
761
+ Set<Address> seen,
762
+ FunctionIterator functions) throws Exception {
763
+ while (functions.hasNext()) {
764
+ monitor.checkCancelled();
765
+ Function function = functions.next();
766
+ if (!seen.add(function.getEntryPoint())) {
767
+ continue;
768
+ }
769
+ JsonObject facts = new JsonObject();
770
+ facts.addProperty("external", function.isExternal());
771
+ facts.addProperty("thunk", function.isThunk());
772
+ Function target = function.isThunk() ? function.getThunkedFunction(false) : null;
773
+ if (target == null) {
774
+ facts.add("thunk_target", JsonNull.INSTANCE);
775
+ }
776
+ else {
777
+ facts.addProperty("thunk_target", canonicalAddress(target.getEntryPoint()));
778
+ }
779
+ Symbol symbol = function.getSymbol();
780
+ InventoryItem item = new InventoryItem(
781
+ function.getEntryPoint(),
782
+ symbol == null ? function.getName() : symbol.getName(true),
783
+ facts
784
+ );
785
+ if (destination.size() >= MAX_INVENTORY_ITEMS) {
786
+ throw inventoryLimit();
787
+ }
788
+ destination.add(new FunctionEntry(function, item));
789
+ }
790
+ }
791
+
792
+ private List<InventoryItem> strings() throws Exception {
793
+ if (stringInventory != null) {
794
+ return stringInventory;
795
+ }
796
+ List<InventoryItem> result = new ArrayList<>();
797
+ DataIterator dataItems = currentProgram.getListing().getDefinedData(true);
798
+ while (dataItems.hasNext()) {
799
+ monitor.checkCancelled();
800
+ Data data = dataItems.next();
801
+ if (!data.hasStringValue()) {
802
+ continue;
803
+ }
804
+ StringDataInstance instance = StringDataInstance.getStringDataInstance(data);
805
+ String value = instance.getStringValue();
806
+ if (value == null) {
807
+ continue;
808
+ }
809
+ JsonObject facts = new JsonObject();
810
+ String charset = instance.getCharsetName();
811
+ facts.addProperty("encoding", charset == null || charset.isEmpty() ? "unknown" : charset);
812
+ facts.addProperty(
813
+ "termination",
814
+ instance.isMissingNullTerminator() ? "missing" : "present_or_not_required"
815
+ );
816
+ facts.addProperty("byte_length", Math.max(0, data.getLength()));
817
+ addBounded(result, new InventoryItem(data.getAddress(), value, facts));
818
+ }
819
+ result.sort(INVENTORY_ORDER);
820
+ stringInventory = List.copyOf(result);
821
+ return stringInventory;
822
+ }
823
+
824
+ private Function resolveProcedure(String value) throws Exception {
825
+ Address address = tryParseAddress(value);
826
+ if (address != null) {
827
+ Function function = currentProgram.getFunctionManager().getFunctionAt(address);
828
+ if (function == null && address.isMemoryAddress()) {
829
+ function = currentProgram.getFunctionManager().getFunctionContaining(address);
830
+ }
831
+ if (function == null) {
832
+ throw new RequestFailure("not_found", "No procedure exists at the requested address");
833
+ }
834
+ return function;
835
+ }
836
+ List<Function> matches = new ArrayList<>();
837
+ for (FunctionEntry entry : procedures()) {
838
+ Function function = entry.function;
839
+ Symbol symbol = function.getSymbol();
840
+ String qualified = symbol == null ? function.getName() : symbol.getName(true);
841
+ if (function.getName().equals(value) || qualified.equals(value)) {
842
+ matches.add(function);
843
+ }
844
+ }
845
+ if (matches.isEmpty()) {
846
+ throw new RequestFailure("not_found", "Unknown Ghidra procedure name");
847
+ }
848
+ if (matches.size() != 1) {
849
+ throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous");
850
+ }
851
+ return matches.get(0);
852
+ }
853
+
854
+ private String decompile(Function function) {
855
+ if (function.isExternal() || function.getBody().isEmpty()) {
856
+ return null;
857
+ }
858
+ DecompileResults results = decompiler.decompileFunction(
859
+ function,
860
+ DECOMPILE_TIMEOUT_SECONDS,
861
+ monitor
862
+ );
863
+ if (results.isTimedOut()) {
864
+ throw new RequestFailure(
865
+ "decompile_timeout",
866
+ "Ghidra decompilation reached its 30-second deadline"
867
+ );
868
+ }
869
+ if (results.isCancelled()) {
870
+ throw new RequestFailure("decompile_cancelled", "Ghidra decompilation was cancelled");
871
+ }
872
+ if (!results.decompileCompleted()) {
873
+ throw new RequestFailure(
874
+ "decompile_failed",
875
+ "Ghidra decompilation failed: " + boundedMessage(results.getErrorMessage())
876
+ );
877
+ }
878
+ DecompiledFunction value = results.getDecompiledFunction();
879
+ return value == null ? null : value.getC();
880
+ }
881
+
882
+ private InstructionScan scanInstructions(Function function, int maximum) throws Exception {
883
+ InstructionIterator iterator = currentProgram.getListing().getInstructions(
884
+ function.getBody(),
885
+ true
886
+ );
887
+ List<Instruction> instructions = new ArrayList<>();
888
+ while (iterator.hasNext() && instructions.size() < maximum) {
889
+ monitor.checkCancelled();
890
+ instructions.add(iterator.next());
891
+ }
892
+ return new InstructionScan(List.copyOf(instructions), iterator.hasNext());
893
+ }
894
+
895
+ private static String renderAssembly(List<Instruction> instructions) {
896
+ return String.join("\n", renderAssemblyLines(instructions));
897
+ }
898
+
899
+ private static List<String> renderAssemblyLines(List<Instruction> instructions) {
900
+ List<String> result = new ArrayList<>();
901
+ for (Instruction instruction : instructions) {
902
+ StringBuilder line = new StringBuilder();
903
+ line.append(canonicalAddress(instruction.getAddress()));
904
+ line.append(": ");
905
+ line.append(instruction.getMnemonicString());
906
+ for (int index = 0; index < instruction.getNumOperands(); index += 1) {
907
+ line.append(index == 0 ? " " : ", ");
908
+ line.append(instruction.getDefaultOperandRepresentation(index));
909
+ }
910
+ result.add(line.toString());
911
+ }
912
+ return result;
913
+ }
914
+
915
+ private List<Reference> collectReferences(
916
+ List<Instruction> instructions,
917
+ String direction) throws Exception {
918
+ TreeMap<String, Reference> observed = new TreeMap<>();
919
+ for (Instruction instruction : instructions) {
920
+ monitor.checkCancelled();
921
+ if (direction.equals("outgoing")) {
922
+ for (Reference reference : currentProgram.getReferenceManager()
923
+ .getReferencesFrom(instruction.getAddress())) {
924
+ addReference(observed, reference);
925
+ }
926
+ }
927
+ else {
928
+ ReferenceIterator iterator = currentProgram.getReferenceManager()
929
+ .getReferencesTo(instruction.getAddress());
930
+ while (iterator.hasNext()) {
931
+ addReference(observed, iterator.next());
932
+ }
933
+ }
934
+ }
935
+ List<Reference> result = new ArrayList<>(observed.values());
936
+ result.sort(REFERENCE_ORDER);
937
+ return result;
938
+ }
939
+
940
+ private static void addReference(TreeMap<String, Reference> observed, Reference reference) {
941
+ if (reference.isEntryPointReference()) {
942
+ return;
943
+ }
944
+ String key = canonicalAddress(reference.getFromAddress()) + "\u0000" +
945
+ canonicalAddress(reference.getToAddress()) + "\u0000" +
946
+ reference.getReferenceType().getName() + "\u0000" +
947
+ reference.getOperandIndex() + "\u0000" +
948
+ reference.isPrimary();
949
+ observed.putIfAbsent(key, reference);
950
+ if (observed.size() > MAX_FUNCTION_ITEMS) {
951
+ throw functionLimit("Reference set exceeds the 100000-item limit");
952
+ }
953
+ }
954
+
955
+ private JsonArray referenceEdges(List<Reference> references) {
956
+ JsonArray result = new JsonArray();
957
+ for (Reference reference : references) {
958
+ result.add(referenceEdge(reference));
959
+ }
960
+ return result;
961
+ }
962
+
963
+ private JsonObject referenceEdge(Reference reference) {
964
+ JsonObject result = new JsonObject();
965
+ result.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
966
+ result.addProperty("target_address", canonicalAddress(reference.getToAddress()));
967
+ Function source = containingFunction(reference.getFromAddress());
968
+ Function target = containingFunction(reference.getToAddress());
969
+ result.add(
970
+ "source_procedure",
971
+ source == null ? JsonNull.INSTANCE : procedureIdentity(source)
972
+ );
973
+ result.add(
974
+ "target_procedure",
975
+ target == null ? JsonNull.INSTANCE : procedureIdentity(target)
976
+ );
977
+ result.add("kind", referenceKind(reference));
978
+ return result;
979
+ }
980
+
981
+ private static JsonObject referenceKind(Reference reference) {
982
+ RefType type = reference.getReferenceType();
983
+ JsonObject result = new JsonObject();
984
+ result.addProperty("available", true);
985
+ result.addProperty("provenance", "ghidra-reference-manager");
986
+ result.addProperty("type", type.getName());
987
+ result.addProperty("flow", type.isFlow());
988
+ result.addProperty("call", type.isCall());
989
+ result.addProperty("jump", type.isJump());
990
+ result.addProperty("data", type.isData());
991
+ result.addProperty("read", type.isRead());
992
+ result.addProperty("write", type.isWrite());
993
+ result.addProperty("indirect", type.isIndirect());
994
+ result.addProperty("computed", type.isComputed());
995
+ result.addProperty("conditional", type.isConditional());
996
+ result.addProperty("terminal", type.isTerminal());
997
+ result.addProperty("primary", reference.isPrimary());
998
+ result.addProperty("operand_index", reference.getOperandIndex());
999
+ result.addProperty("external", reference.isExternalReference());
1000
+ return result;
1001
+ }
1002
+
1003
+ private Function containingFunction(Address address) {
1004
+ Function result = currentProgram.getFunctionManager().getFunctionAt(address);
1005
+ if (result == null && address.isMemoryAddress()) {
1006
+ result = currentProgram.getFunctionManager().getFunctionContaining(address);
1007
+ }
1008
+ return result;
1009
+ }
1010
+
1011
+ private static String procedureName(Function function) {
1012
+ Symbol symbol = function.getSymbol();
1013
+ return symbol == null ? function.getName() : symbol.getName(true);
1014
+ }
1015
+
1016
+ private static JsonObject functionClassification(Function function) {
1017
+ JsonObject result = new JsonObject();
1018
+ result.addProperty("external", function.isExternal());
1019
+ result.addProperty("thunk", function.isThunk());
1020
+ Function target = function.isThunk() ? function.getThunkedFunction(false) : null;
1021
+ if (target == null) {
1022
+ result.add("thunk_target", JsonNull.INSTANCE);
1023
+ }
1024
+ else {
1025
+ result.addProperty("thunk_target", canonicalAddress(target.getEntryPoint()));
1026
+ }
1027
+ result.addProperty("provenance", "ghidra-function-manager");
1028
+ return result;
1029
+ }
1030
+
1031
+ private static JsonArray functionLocals(Function function) {
1032
+ Variable[] variables = function.getLocalVariables();
1033
+ if (variables.length > MAX_FUNCTION_ITEMS) {
1034
+ throw functionLimit("Local-variable set exceeds the 100000-item limit");
1035
+ }
1036
+ List<Variable> ordered = new ArrayList<>(List.of(variables));
1037
+ ordered.sort(
1038
+ Comparator.comparing(Variable::getName)
1039
+ .thenComparing(variable -> variable.getVariableStorage().toString())
1040
+ );
1041
+ JsonArray result = new JsonArray();
1042
+ for (Variable variable : ordered) {
1043
+ JsonObject item = new JsonObject();
1044
+ item.addProperty(
1045
+ "description",
1046
+ variable.getDataType().getDisplayName() + " " + variable.getName() +
1047
+ " @ " + variable.getVariableStorage()
1048
+ );
1049
+ item.addProperty("provenance", "ghidra-function-database");
1050
+ result.add(item);
1051
+ }
1052
+ return result;
1053
+ }
1054
+
1055
+ private JsonArray procedureIdentities(Set<Function> functions) {
1056
+ if (functions.size() > MAX_FUNCTION_ITEMS) {
1057
+ throw functionLimit("Resolved call set exceeds the 100000-item limit");
1058
+ }
1059
+ List<Function> ordered = new ArrayList<>(functions);
1060
+ ordered.sort(Comparator.comparing(Function::getEntryPoint));
1061
+ JsonArray result = new JsonArray();
1062
+ for (Function function : ordered) {
1063
+ result.add(procedureIdentity(function));
1064
+ }
1065
+ return result;
1066
+ }
1067
+
1068
+ private List<CodeBlock> basicBlocks(Function function) throws Exception {
1069
+ BasicBlockModel model = new BasicBlockModel(currentProgram, false);
1070
+ CodeBlockIterator iterator = model.getCodeBlocksContaining(function.getBody(), monitor);
1071
+ List<CodeBlock> result = new ArrayList<>();
1072
+ while (iterator.hasNext()) {
1073
+ monitor.checkCancelled();
1074
+ if (result.size() >= MAX_FUNCTION_ITEMS) {
1075
+ throw functionLimit("Basic-block set exceeds the 100000-item limit");
1076
+ }
1077
+ result.add(iterator.next());
1078
+ }
1079
+ result.sort(Comparator.comparing(CodeBlock::getFirstStartAddress));
1080
+ return result;
1081
+ }
1082
+
1083
+ private JsonArray basicBlockValues(Function function) throws Exception {
1084
+ JsonArray result = new JsonArray();
1085
+ for (CodeBlock block : basicBlocks(function)) {
1086
+ JsonObject item = new JsonObject();
1087
+ item.addProperty("start", canonicalAddress(block.getFirstStartAddress()));
1088
+ item.addProperty("end", exclusiveAddress(block.getMaxAddress()));
1089
+ Set<Address> successors = new HashSet<>();
1090
+ CodeBlockReferenceIterator destinations = block.getDestinations(monitor);
1091
+ while (destinations.hasNext()) {
1092
+ CodeBlockReference destination = destinations.next();
1093
+ CodeBlock destinationBlock = destination.getDestinationBlock();
1094
+ if (destinationBlock == null) {
1095
+ continue;
1096
+ }
1097
+ Address address = destinationBlock.getFirstStartAddress();
1098
+ if (
1099
+ !destination.getFlowType().isCall() &&
1100
+ function.getBody().contains(address)
1101
+ ) {
1102
+ successors.add(address);
1103
+ }
1104
+ }
1105
+ List<Address> ordered = new ArrayList<>(successors);
1106
+ ordered.sort(Address::compareTo);
1107
+ JsonArray values = new JsonArray();
1108
+ for (Address address : ordered) {
1109
+ values.add(canonicalAddress(address));
1110
+ }
1111
+ item.add("successors", values);
1112
+ result.add(item);
1113
+ }
1114
+ return result;
1115
+ }
1116
+
1117
+ private static JsonArray comments(List<Instruction> instructions) {
1118
+ TreeMap<String, JsonObject> observed = new TreeMap<>();
1119
+ for (Instruction instruction : instructions) {
1120
+ for (CommentType type : CommentType.values()) {
1121
+ String text = instruction.getComment(type);
1122
+ if (text == null || text.isEmpty()) {
1123
+ continue;
1124
+ }
1125
+ String kind = type == CommentType.EOL ? "inline" : "comment";
1126
+ JsonObject item = new JsonObject();
1127
+ item.addProperty("address", canonicalAddress(instruction.getAddress()));
1128
+ item.addProperty("kind", kind);
1129
+ item.addProperty("text", text);
1130
+ String key = canonicalAddress(instruction.getAddress()) + "\u0000" +
1131
+ kind + "\u0000" + type.name() + "\u0000" + text;
1132
+ observed.put(key, item);
1133
+ if (observed.size() > MAX_FUNCTION_ITEMS) {
1134
+ throw functionLimit("Comment set exceeds the 100000-item limit");
1135
+ }
1136
+ }
1137
+ }
1138
+ JsonArray result = new JsonArray();
1139
+ observed.values().forEach(result::add);
1140
+ return result;
1141
+ }
1142
+
1143
+ private JsonArray referencedStrings(List<Reference> references) {
1144
+ TreeMap<String, JsonObject> observed = new TreeMap<>();
1145
+ for (Reference reference : references) {
1146
+ Data data = currentProgram.getListing().getDefinedDataContaining(
1147
+ reference.getToAddress()
1148
+ );
1149
+ if (data == null || !data.hasStringValue()) {
1150
+ continue;
1151
+ }
1152
+ String value = StringDataInstance.getStringDataInstance(data).getStringValue();
1153
+ if (value == null) {
1154
+ continue;
1155
+ }
1156
+ JsonObject item = new JsonObject();
1157
+ item.addProperty("address", canonicalAddress(data.getAddress()));
1158
+ item.addProperty("value", value);
1159
+ item.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
1160
+ String key = canonicalAddress(data.getAddress()) + "\u0000" + value + "\u0000" +
1161
+ canonicalAddress(reference.getFromAddress());
1162
+ observed.put(key, item);
1163
+ if (observed.size() > MAX_FUNCTION_ITEMS) {
1164
+ throw functionLimit("Referenced-string set exceeds the 100000-item limit");
1165
+ }
1166
+ }
1167
+ JsonArray result = new JsonArray();
1168
+ observed.values().forEach(result::add);
1169
+ return result;
1170
+ }
1171
+
1172
+ private JsonArray referencedNames(List<Reference> references) {
1173
+ TreeMap<String, JsonObject> observed = new TreeMap<>();
1174
+ for (Reference reference : references) {
1175
+ Symbol symbol = currentProgram.getSymbolTable().getPrimarySymbol(
1176
+ reference.getToAddress()
1177
+ );
1178
+ if (symbol == null) {
1179
+ continue;
1180
+ }
1181
+ JsonObject item = new JsonObject();
1182
+ item.addProperty("address", canonicalAddress(reference.getToAddress()));
1183
+ item.addProperty("value", symbol.getName(true));
1184
+ item.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
1185
+ String key = canonicalAddress(reference.getToAddress()) + "\u0000" +
1186
+ symbol.getName(true) + "\u0000" + canonicalAddress(reference.getFromAddress());
1187
+ observed.put(key, item);
1188
+ if (observed.size() > MAX_FUNCTION_ITEMS) {
1189
+ throw functionLimit("Referenced-name set exceeds the 100000-item limit");
1190
+ }
1191
+ }
1192
+ JsonArray result = new JsonArray();
1193
+ observed.values().forEach(result::add);
1194
+ return result;
1195
+ }
1196
+
1197
+ private static JsonObject pseudocodePage(String value, int offset, int limit) {
1198
+ int total = value.codePointCount(0, value.length());
1199
+ int start = Math.min(offset, total);
1200
+ int returned = Math.min(limit, total - start);
1201
+ int startIndex = value.offsetByCodePoints(0, start);
1202
+ int endIndex = value.offsetByCodePoints(startIndex, returned);
1203
+ int next = start + returned;
1204
+ JsonObject result = new JsonObject();
1205
+ result.addProperty("text", value.substring(startIndex, endIndex));
1206
+ result.addProperty("total_chars", total);
1207
+ result.addProperty("returned_chars", returned);
1208
+ result.addProperty("truncated", next < total);
1209
+ if (next < total) {
1210
+ result.addProperty("next_offset", next);
1211
+ }
1212
+ else {
1213
+ result.add("next_offset", JsonNull.INSTANCE);
1214
+ }
1215
+ return result;
1216
+ }
1217
+
1218
+ private static JsonObject bounded(
1219
+ JsonArray values,
1220
+ int offset,
1221
+ int limit,
1222
+ boolean complete) {
1223
+ int start = Math.min(offset, values.size());
1224
+ int end = Math.min(values.size(), start + limit);
1225
+ JsonArray items = new JsonArray();
1226
+ for (int index = start; index < end; index += 1) {
1227
+ items.add(values.get(index).deepCopy());
1228
+ }
1229
+ boolean hasMore = end < values.size();
1230
+ JsonObject result = new JsonObject();
1231
+ result.add("items", items);
1232
+ if (complete) {
1233
+ result.addProperty("total", values.size());
1234
+ }
1235
+ else {
1236
+ result.add("total", JsonNull.INSTANCE);
1237
+ }
1238
+ result.addProperty("returned", items.size());
1239
+ result.addProperty("truncated", !complete || hasMore);
1240
+ if (complete && hasMore) {
1241
+ result.addProperty("next_offset", end);
1242
+ }
1243
+ else {
1244
+ result.add("next_offset", JsonNull.INSTANCE);
1245
+ }
1246
+ return result;
1247
+ }
1248
+
1249
+ private static JsonObject requireCollectionOffsets(JsonObject params) {
1250
+ JsonElement value = params.get("collection_offset");
1251
+ if (value == null || !value.isJsonObject()) {
1252
+ throw new RequestFailure("invalid_request", "collection_offset must be an object");
1253
+ }
1254
+ JsonObject result = value.getAsJsonObject();
1255
+ requireKeys(
1256
+ result,
1257
+ Set.of(
1258
+ "comments",
1259
+ "callers",
1260
+ "callees",
1261
+ "incoming_references",
1262
+ "outgoing_references",
1263
+ "referenced_strings",
1264
+ "referenced_names",
1265
+ "basic_blocks"
1266
+ )
1267
+ );
1268
+ for (String name : result.keySet()) {
1269
+ requireBoundedInteger(result, name, 0, Integer.MAX_VALUE);
1270
+ }
1271
+ return result;
1272
+ }
1273
+
1274
+ private static int collectionOffset(JsonObject offsets, String name) {
1275
+ return requireBoundedInteger(offsets, name, 0, Integer.MAX_VALUE);
1276
+ }
1277
+
1278
+ private static String exclusiveAddress(Address inclusive) {
1279
+ Address next = inclusive.next();
1280
+ if (next != null && next.getAddressSpace().equals(inclusive.getAddressSpace())) {
1281
+ return canonicalAddress(next);
1282
+ }
1283
+ BigInteger offset = new BigInteger(Long.toUnsignedString(inclusive.getOffset()));
1284
+ return canonicalOffset(inclusive.getAddressSpace(), offset.add(BigInteger.ONE));
1285
+ }
1286
+
1287
+ private static String boundedMessage(String value) {
1288
+ if (value == null || value.isBlank()) {
1289
+ return "native decompiler returned no detail";
1290
+ }
1291
+ return truncate(value.replaceAll("[\\r\\n]+", " "), 512).value;
1292
+ }
1293
+
1294
+ private static RequestFailure functionLimit(String message) {
1295
+ return new RequestFailure("limit_exceeded", message);
1296
+ }
1297
+
1298
+ private JsonObject page(
1299
+ List<InventoryItem> inventory,
1300
+ int offset,
1301
+ int limit,
1302
+ String factsName) {
1303
+ int start = Math.min(offset, inventory.size());
1304
+ int end = Math.min(inventory.size(), start + limit);
1305
+ JsonArray items = new JsonArray();
1306
+ for (int index = start; index < end; index += 1) {
1307
+ items.add(resultItem(inventory.get(index), factsName, MAX_LIST_VALUE_CODE_POINTS));
1308
+ }
1309
+ return pageResult(items, offset, limit, inventory.size());
1310
+ }
1311
+
1312
+ private static JsonObject pageResult(JsonArray items, int offset, int limit, int total) {
1313
+ int next = offset + items.size();
1314
+ boolean hasMore = next < total;
1315
+ JsonObject result = new JsonObject();
1316
+ result.add("items", items);
1317
+ result.addProperty("offset", offset);
1318
+ result.addProperty("limit", limit);
1319
+ result.addProperty("total", total);
1320
+ if (hasMore) {
1321
+ result.addProperty("next_offset", next);
1322
+ }
1323
+ else {
1324
+ result.add("next_offset", JsonNull.INSTANCE);
1325
+ }
1326
+ result.addProperty("has_more", hasMore);
1327
+ return result;
1328
+ }
1329
+
1330
+ private static JsonObject resultItem(
1331
+ InventoryItem item,
1332
+ String factsName,
1333
+ int maximumCodePoints) {
1334
+ TruncatedValue truncated = truncate(item.value, maximumCodePoints);
1335
+ JsonObject result = new JsonObject();
1336
+ result.addProperty("address", canonicalAddress(item.address));
1337
+ result.addProperty("value", truncated.value);
1338
+ result.addProperty("value_truncated", truncated.truncated);
1339
+ if (factsName != null) {
1340
+ result.add(factsName, item.facts.deepCopy());
1341
+ }
1342
+ return result;
1343
+ }
1344
+
1345
+ private static JsonObject memoryRegion(MemoryBlock block, String imageBase) {
1346
+ JsonObject permissions = new JsonObject();
1347
+ permissions.addProperty("available", true);
1348
+ permissions.addProperty("source", "ghidra-memory-block");
1349
+ JsonObject result = new JsonObject();
1350
+ result.addProperty("name", block.getName());
1351
+ result.addProperty("start", canonicalAddress(block.getStart()));
1352
+ result.addProperty("end", exclusiveEnd(block));
1353
+ result.addProperty("readable", block.isRead());
1354
+ result.addProperty("writable", block.isWrite());
1355
+ result.addProperty("executable", block.isExecute());
1356
+ result.add("permissions", permissions);
1357
+ result.addProperty("provenance", "ghidra-memory-block");
1358
+ result.addProperty("address_space", block.getStart().getAddressSpace().getName());
1359
+ result.addProperty("image_base", imageBase);
1360
+ result.addProperty("initialized", block.isInitialized());
1361
+ result.addProperty("overlay", block.isOverlay());
1362
+ return result;
1363
+ }
1364
+
1365
+ private static JsonObject procedureIdentity(Function function) {
1366
+ JsonObject result = new JsonObject();
1367
+ result.addProperty("address", canonicalAddress(function.getEntryPoint()));
1368
+ result.addProperty("name", procedureName(function));
1369
+ result.add("classification", functionClassification(function));
1370
+ return result;
1371
+ }
1372
+
1373
+ private static String exclusiveEnd(MemoryBlock block) {
1374
+ Address next = block.getEnd().next();
1375
+ if (next != null && next.getAddressSpace().equals(block.getStart().getAddressSpace())) {
1376
+ return canonicalAddress(next);
1377
+ }
1378
+ BigInteger start = new BigInteger(Long.toUnsignedString(block.getStart().getOffset()));
1379
+ return canonicalOffset(
1380
+ block.getStart().getAddressSpace(),
1381
+ start.add(BigInteger.valueOf(block.getSize()))
1382
+ );
1383
+ }
1384
+
1385
+ private static String canonicalAddress(Address address) {
1386
+ return canonicalOffset(
1387
+ address.getAddressSpace(),
1388
+ new BigInteger(Long.toUnsignedString(address.getOffset()))
1389
+ );
1390
+ }
1391
+
1392
+ private static String canonicalOffset(AddressSpace space, BigInteger offset) {
1393
+ String value = "0x" + offset.toString(16);
1394
+ if (space.equals(sessionDefaultAddressSpace)) {
1395
+ return value;
1396
+ }
1397
+ return encodeAddressSpace(space.getName()) + ":" + value;
1398
+ }
1399
+
1400
+ private Address requireAddress(JsonObject params, String name) {
1401
+ return parseReaAddress(requireString(params, name));
1402
+ }
1403
+
1404
+ private Address parseReaAddress(String value) {
1405
+ Address address = tryParseAddress(value);
1406
+ if (address == null) {
1407
+ throw new RequestFailure("invalid_request", "Unknown or invalid Ghidra address");
1408
+ }
1409
+ return address;
1410
+ }
1411
+
1412
+ private Address tryParseAddress(String value) {
1413
+ try {
1414
+ String spaceName = null;
1415
+ String offset = value;
1416
+ int separator = value.lastIndexOf(":0x");
1417
+ if (separator >= 0) {
1418
+ spaceName = decodeAddressSpace(value.substring(0, separator));
1419
+ offset = value.substring(separator + 3);
1420
+ }
1421
+ else if (value.startsWith("0x")) {
1422
+ offset = value.substring(2);
1423
+ }
1424
+ if (!offset.matches("[0-9A-Fa-f]+")) {
1425
+ return null;
1426
+ }
1427
+ AddressSpace space = spaceName == null
1428
+ ? currentProgram.getAddressFactory().getDefaultAddressSpace()
1429
+ : currentProgram.getAddressFactory().getAddressSpace(spaceName);
1430
+ return space == null ? null : space.getAddress(offset);
1431
+ }
1432
+ catch (Exception exception) {
1433
+ return null;
1434
+ }
1435
+ }
1436
+
1437
+ private static String encodeAddressSpace(String value) {
1438
+ StringBuilder result = new StringBuilder();
1439
+ for (byte item : value.getBytes(StandardCharsets.UTF_8)) {
1440
+ int unsigned = Byte.toUnsignedInt(item);
1441
+ if ((unsigned >= 'A' && unsigned <= 'Z') ||
1442
+ (unsigned >= 'a' && unsigned <= 'z') ||
1443
+ (unsigned >= '0' && unsigned <= '9') ||
1444
+ unsigned == '.' || unsigned == '_' || unsigned == '~' || unsigned == '-') {
1445
+ result.append((char) unsigned);
1446
+ }
1447
+ else {
1448
+ result.append('%');
1449
+ result.append(Character.toUpperCase(Character.forDigit(unsigned >>> 4, 16)));
1450
+ result.append(Character.toUpperCase(Character.forDigit(unsigned & 0xf, 16)));
1451
+ }
1452
+ }
1453
+ return result.toString();
1454
+ }
1455
+
1456
+ private static String decodeAddressSpace(String value) {
1457
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
1458
+ for (int index = 0; index < value.length();) {
1459
+ char item = value.charAt(index);
1460
+ if (item == '%') {
1461
+ if (index + 2 >= value.length()) {
1462
+ throw new RequestFailure("invalid_request", "Address space encoding is invalid");
1463
+ }
1464
+ int high = Character.digit(value.charAt(index + 1), 16);
1465
+ int low = Character.digit(value.charAt(index + 2), 16);
1466
+ if (high < 0 || low < 0) {
1467
+ throw new RequestFailure("invalid_request", "Address space encoding is invalid");
1468
+ }
1469
+ bytes.write((high << 4) | low);
1470
+ index += 3;
1471
+ }
1472
+ else if ((item >= 'A' && item <= 'Z') ||
1473
+ (item >= 'a' && item <= 'z') ||
1474
+ (item >= '0' && item <= '9') ||
1475
+ item == '.' || item == '_' || item == '~' || item == '-') {
1476
+ bytes.write((byte) item);
1477
+ index += 1;
1478
+ }
1479
+ else {
1480
+ throw new RequestFailure("invalid_request", "Address space encoding is invalid");
1481
+ }
1482
+ }
1483
+ return bytes.toString(StandardCharsets.UTF_8);
1484
+ }
1485
+
1486
+ private static String normalizedSymbolType(Symbol symbol) {
1487
+ String value = symbol.getSymbolType().toString()
1488
+ .toLowerCase(Locale.ROOT)
1489
+ .replaceAll("[^a-z0-9]+", "_")
1490
+ .replaceAll("^_+|_+$", "");
1491
+ return value.isEmpty() ? "unknown" : value;
1492
+ }
1493
+
1494
+ private static String normalizedSource(SourceType source) {
1495
+ return source.name().toLowerCase(Locale.ROOT);
1496
+ }
1497
+
1498
+ private static void addBounded(List<InventoryItem> destination, InventoryItem item) {
1499
+ if (destination.size() >= MAX_INVENTORY_ITEMS) {
1500
+ throw inventoryLimit();
1501
+ }
1502
+ destination.add(item);
1503
+ }
1504
+
1505
+ private static RequestFailure inventoryLimit() {
1506
+ return new RequestFailure(
1507
+ "limit_exceeded",
1508
+ "Ghidra inventory exceeds the 1000000-item safety limit"
1509
+ );
1510
+ }
1511
+
1512
+ private static TruncatedValue truncate(String value, int maximumCodePoints) {
1513
+ int codePoints = value.codePointCount(0, value.length());
1514
+ if (codePoints <= maximumCodePoints) {
1515
+ return new TruncatedValue(value, false);
1516
+ }
1517
+ int end = value.offsetByCodePoints(0, maximumCodePoints);
1518
+ return new TruncatedValue(value.substring(0, end), true);
1519
+ }
1520
+
1521
+ private static ValueMatcher literalMatcher(String pattern, boolean caseSensitive) {
1522
+ String needle = caseSensitive ? pattern : pattern.toLowerCase(Locale.ROOT);
1523
+ return new ValueMatcher() {
1524
+ private long remaining = MAX_LITERAL_SEARCH_WORK_UNITS;
1525
+
1526
+ @Override
1527
+ public boolean matches(String value) {
1528
+ long required = (long) Math.max(value.length(), 1) + needle.length();
1529
+ if (required > remaining) {
1530
+ throw new RequestFailure(
1531
+ "limit_exceeded",
1532
+ "Literal search exceeds the 1000000-unit work budget"
1533
+ );
1534
+ }
1535
+ remaining -= required;
1536
+ String candidate = caseSensitive ? value : value.toLowerCase(Locale.ROOT);
1537
+ return candidate.contains(needle);
1538
+ }
1539
+ };
1540
+ }
1541
+
1542
+ private static ValueMatcher regexMatcher(String expression, boolean caseSensitive) {
1543
+ RegexMetrics metrics;
1544
+ Pattern pattern;
1545
+ try {
1546
+ metrics = new BoundedRegexParser(expression).parse();
1547
+ pattern = Pattern.compile(
1548
+ expression,
1549
+ caseSensitive ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE
1550
+ );
1551
+ }
1552
+ catch (PatternSyntaxException exception) {
1553
+ throw new RequestFailure("invalid_request", "Invalid regex pattern");
1554
+ }
1555
+ long workPerCharacter = metrics.paths * Math.max(metrics.steps, 1);
1556
+ return new ValueMatcher() {
1557
+ private long remaining = MAX_REGEX_SEARCH_WORK_UNITS;
1558
+
1559
+ @Override
1560
+ public boolean matches(String value) {
1561
+ if (value.length() > MAX_REGEX_CANDIDATE_CHARACTERS) {
1562
+ throw new RequestFailure(
1563
+ "limit_exceeded",
1564
+ "Regex candidate exceeds the 4096-character safety limit"
1565
+ );
1566
+ }
1567
+ long required = workPerCharacter * Math.max(value.length(), 1);
1568
+ if (required > remaining) {
1569
+ throw new RequestFailure(
1570
+ "limit_exceeded",
1571
+ "Regex search exceeds the 1000000-unit work budget"
1572
+ );
1573
+ }
1574
+ remaining -= required;
1575
+ return pattern.matcher(value).find();
1576
+ }
1577
+ };
1578
+ }
1579
+
1580
+ private static SessionDescriptor readDescriptor(Path path) throws IOException {
1581
+ if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) ||
1582
+ Files.size(path) > MAX_DESCRIPTOR_BYTES) {
1583
+ throw new IllegalArgumentException("REA session descriptor is invalid");
1584
+ }
1585
+ JsonObject object = requireObject(
1586
+ JsonParser.parseString(Files.readString(path, StandardCharsets.UTF_8)),
1587
+ DESCRIPTOR_KEYS
1588
+ );
1589
+ if (requireInteger(object, "schema_version") != 1) {
1590
+ throw new IllegalArgumentException("REA session descriptor version is invalid");
1591
+ }
1592
+ return new SessionDescriptor(
1593
+ requireString(object, "socket_path"),
1594
+ requireString(object, "token"),
1595
+ requireString(object, "run_id"),
1596
+ requireString(object, "provider_version"),
1597
+ requireString(object, "profile_digest")
1598
+ );
1599
+ }
1600
+
1601
+ private static Request parseRequest(String line, String expectedToken) {
1602
+ JsonObject object = requireObject(JsonParser.parseString(line), REQUEST_KEYS);
1603
+ int id = requireInteger(object, "id");
1604
+ if (id <= 0 || !constantTimeEquals(requireString(object, "token"), expectedToken)) {
1605
+ throw new IllegalArgumentException("Bridge request authentication failed");
1606
+ }
1607
+ JsonElement params = object.get("params");
1608
+ if (params == null || !params.isJsonObject()) {
1609
+ throw new IllegalArgumentException("Bridge request parameters are invalid");
1610
+ }
1611
+ return new Request(id, requireString(object, "method"), params.getAsJsonObject());
1612
+ }
1613
+
1614
+ private static JsonObject requireObject(JsonElement element, Set<String> keys) {
1615
+ if (!element.isJsonObject()) {
1616
+ throw new IllegalArgumentException("JSON object required");
1617
+ }
1618
+ JsonObject object = element.getAsJsonObject();
1619
+ if (!object.keySet().equals(keys)) {
1620
+ throw new IllegalArgumentException("JSON object fields are invalid");
1621
+ }
1622
+ return object;
1623
+ }
1624
+
1625
+ private static void requireKeys(JsonObject object, Set<String> keys) {
1626
+ if (!object.keySet().equals(keys)) {
1627
+ throw new RequestFailure("invalid_request", "Bridge request fields are invalid");
1628
+ }
1629
+ }
1630
+
1631
+ private void requireDocument(JsonObject params) {
1632
+ String document = optionalString(params, "document");
1633
+ if (document != null && !document.equals(currentProgram.getName())) {
1634
+ throw new RequestFailure("not_found", "Unknown Ghidra Program identity");
1635
+ }
1636
+ }
1637
+
1638
+ private static String optionalString(JsonObject object, String name) {
1639
+ JsonElement value = object.get(name);
1640
+ if (value == null || value.isJsonNull()) {
1641
+ return null;
1642
+ }
1643
+ if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
1644
+ throw new RequestFailure("invalid_request", name + " must be a string or null");
1645
+ }
1646
+ String result = value.getAsString();
1647
+ if (result.isEmpty()) {
1648
+ throw new RequestFailure("invalid_request", name + " cannot be empty");
1649
+ }
1650
+ return result;
1651
+ }
1652
+
1653
+ private static String requireString(JsonObject object, String name) {
1654
+ JsonElement value = object.get(name);
1655
+ if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
1656
+ throw new RequestFailure("invalid_request", name + " must be a string");
1657
+ }
1658
+ String result = value.getAsString();
1659
+ if (result.isEmpty()) {
1660
+ throw new RequestFailure("invalid_request", name + " cannot be empty");
1661
+ }
1662
+ return result;
1663
+ }
1664
+
1665
+ private static int requireInteger(JsonObject object, String name) {
1666
+ JsonElement value = object.get(name);
1667
+ if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) {
1668
+ throw new IllegalArgumentException("JSON integer required");
1669
+ }
1670
+ int result = value.getAsInt();
1671
+ if (value.getAsDouble() != result) {
1672
+ throw new IllegalArgumentException("JSON integer required");
1673
+ }
1674
+ return result;
1675
+ }
1676
+
1677
+ private static int requireBoundedInteger(
1678
+ JsonObject object,
1679
+ String name,
1680
+ int minimum,
1681
+ int maximum) {
1682
+ try {
1683
+ int value = requireInteger(object, name);
1684
+ if (value < minimum || value > maximum) {
1685
+ throw new RequestFailure(
1686
+ "invalid_request",
1687
+ name + " is outside its supported range"
1688
+ );
1689
+ }
1690
+ return value;
1691
+ }
1692
+ catch (IllegalArgumentException exception) {
1693
+ throw new RequestFailure("invalid_request", name + " must be an integer");
1694
+ }
1695
+ }
1696
+
1697
+ private static boolean requireBoolean(JsonObject object, String name) {
1698
+ JsonElement value = object.get(name);
1699
+ if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) {
1700
+ throw new RequestFailure("invalid_request", name + " must be a boolean");
1701
+ }
1702
+ return value.getAsBoolean();
1703
+ }
1704
+
1705
+ private static boolean constantTimeEquals(String provided, String expected) {
1706
+ return MessageDigest.isEqual(
1707
+ provided.getBytes(StandardCharsets.UTF_8),
1708
+ expected.getBytes(StandardCharsets.UTF_8)
1709
+ );
1710
+ }
1711
+
1712
+ private static String readBoundedLine(Reader reader, int maximumCharacters) throws IOException {
1713
+ StringBuilder line = new StringBuilder();
1714
+ while (true) {
1715
+ int character = reader.read();
1716
+ if (character < 0) {
1717
+ return line.isEmpty() ? null : line.toString();
1718
+ }
1719
+ if (character == '\n') {
1720
+ return line.toString();
1721
+ }
1722
+ if (character != '\r') {
1723
+ if (line.length() >= maximumCharacters) {
1724
+ throw new IOException("Bridge request exceeded the maximum line size");
1725
+ }
1726
+ line.append((char) character);
1727
+ }
1728
+ }
1729
+ }
1730
+
1731
+ private static void writeSuccess(BufferedWriter writer, int id, JsonElement result)
1732
+ throws IOException {
1733
+ JsonObject response = new JsonObject();
1734
+ response.addProperty("id", id);
1735
+ response.addProperty("ok", true);
1736
+ response.add("result", result);
1737
+ writeResponse(writer, response);
1738
+ }
1739
+
1740
+ private static void writeFailure(
1741
+ BufferedWriter writer,
1742
+ int id,
1743
+ String code,
1744
+ String message) throws IOException {
1745
+ JsonObject error = new JsonObject();
1746
+ error.addProperty("code", code);
1747
+ error.addProperty("message", message);
1748
+ JsonObject response = new JsonObject();
1749
+ response.addProperty("id", id);
1750
+ response.addProperty("ok", false);
1751
+ response.add("error", error);
1752
+ writeResponse(writer, response);
1753
+ }
1754
+
1755
+ private static void writeResponse(BufferedWriter writer, JsonObject response) throws IOException {
1756
+ String encoded = GSON.toJson(response);
1757
+ if (encoded.getBytes(StandardCharsets.UTF_8).length > MAX_RESPONSE_BYTES) {
1758
+ int id = response.get("id").getAsInt();
1759
+ JsonObject error = new JsonObject();
1760
+ error.addProperty("code", "output_limit");
1761
+ error.addProperty("message", "Ghidra response exceeds the 1 MiB wire limit");
1762
+ JsonObject bounded = new JsonObject();
1763
+ bounded.addProperty("id", id);
1764
+ bounded.addProperty("ok", false);
1765
+ bounded.add("error", error);
1766
+ encoded = GSON.toJson(bounded);
1767
+ }
1768
+ writer.write(encoded);
1769
+ writer.newLine();
1770
+ writer.flush();
1771
+ }
1772
+
1773
+ private static String safeMessage(Exception exception) {
1774
+ String message = exception.getMessage();
1775
+ return message == null || message.isBlank() ? "operation failed" : message;
1776
+ }
1777
+
1778
+ private interface ValueMatcher {
1779
+ boolean matches(String value);
1780
+ }
1781
+
1782
+ private static final Comparator<InventoryItem> INVENTORY_ORDER =
1783
+ Comparator.comparing(InventoryItem::address)
1784
+ .thenComparing(InventoryItem::value)
1785
+ .thenComparing(item -> GSON.toJson(item.facts));
1786
+ private static final Comparator<Reference> REFERENCE_ORDER =
1787
+ Comparator.comparing(Reference::getFromAddress)
1788
+ .thenComparing(Reference::getToAddress)
1789
+ .thenComparing(reference -> reference.getReferenceType().getName())
1790
+ .thenComparingInt(Reference::getOperandIndex)
1791
+ .thenComparing(Reference::isPrimary);
1792
+
1793
+ private record InventoryItem(Address address, String value, JsonObject facts) {}
1794
+ private record FunctionEntry(Function function, InventoryItem item) {}
1795
+ private record InstructionScan(List<Instruction> instructions, boolean truncated) {}
1796
+ private record TruncatedValue(String value, boolean truncated) {}
1797
+ private record SessionDescriptor(
1798
+ String socketPath,
1799
+ String token,
1800
+ String runId,
1801
+ String providerVersion,
1802
+ String profileDigest
1803
+ ) {}
1804
+ private record Request(int id, String method, JsonObject params) {}
1805
+ private record RegexMetrics(long paths, long steps, boolean containsRepeat) {}
1806
+
1807
+ private static final class RequestFailure extends RuntimeException {
1808
+ private final String code;
1809
+
1810
+ RequestFailure(String code, String message) {
1811
+ super(message);
1812
+ this.code = code;
1813
+ }
1814
+ }
1815
+
1816
+ private static final class BoundedRegexParser {
1817
+ private final String expression;
1818
+ private int index;
1819
+
1820
+ BoundedRegexParser(String expression) {
1821
+ this.expression = expression;
1822
+ }
1823
+
1824
+ RegexMetrics parse() {
1825
+ RegexMetrics result = parseExpression(false);
1826
+ if (index != expression.length()) {
1827
+ throw invalidRegex();
1828
+ }
1829
+ return result;
1830
+ }
1831
+
1832
+ private RegexMetrics parseExpression(boolean grouped) {
1833
+ RegexMetrics result = parseSequence(grouped);
1834
+ while (peek('|')) {
1835
+ index += 1;
1836
+ RegexMetrics branch = parseSequence(grouped);
1837
+ result = new RegexMetrics(
1838
+ checkedPaths(result.paths, branch.paths, false),
1839
+ Math.max(result.steps, branch.steps),
1840
+ result.containsRepeat || branch.containsRepeat
1841
+ );
1842
+ }
1843
+ return result;
1844
+ }
1845
+
1846
+ private RegexMetrics parseSequence(boolean grouped) {
1847
+ RegexMetrics result = new RegexMetrics(1, 0, false);
1848
+ while (index < expression.length() && !peek('|') && !(grouped && peek(')'))) {
1849
+ RegexMetrics item = parseAtom();
1850
+ result = new RegexMetrics(
1851
+ checkedPaths(result.paths, item.paths, true),
1852
+ result.steps + item.steps,
1853
+ result.containsRepeat || item.containsRepeat
1854
+ );
1855
+ }
1856
+ return result;
1857
+ }
1858
+
1859
+ private RegexMetrics parseAtom() {
1860
+ if (index >= expression.length()) {
1861
+ throw invalidRegex();
1862
+ }
1863
+ char item = expression.charAt(index++);
1864
+ RegexMetrics atom;
1865
+ if (item == '(') {
1866
+ if (peek('?')) {
1867
+ if (index + 1 >= expression.length() || expression.charAt(index + 1) != ':') {
1868
+ throw new RequestFailure(
1869
+ "invalid_request",
1870
+ "Regex lookarounds and backreferences are not supported"
1871
+ );
1872
+ }
1873
+ index += 2;
1874
+ }
1875
+ atom = parseExpression(true);
1876
+ if (!peek(')')) {
1877
+ throw invalidRegex();
1878
+ }
1879
+ index += 1;
1880
+ }
1881
+ else if (item == '[') {
1882
+ consumeCharacterClass();
1883
+ atom = leaf();
1884
+ }
1885
+ else if (item == '\\') {
1886
+ consumeEscape();
1887
+ atom = leaf();
1888
+ }
1889
+ else if (item == '*' || item == '+') {
1890
+ throw unboundedRepeat();
1891
+ }
1892
+ else if (item == '?' || item == '{' || item == '}' || item == ')') {
1893
+ throw invalidRegex();
1894
+ }
1895
+ else {
1896
+ atom = leaf();
1897
+ }
1898
+ if (index >= expression.length()) {
1899
+ return atom;
1900
+ }
1901
+ if (peek('*') || peek('+')) {
1902
+ throw unboundedRepeat();
1903
+ }
1904
+ int minimum;
1905
+ int maximum;
1906
+ if (peek('?')) {
1907
+ index += 1;
1908
+ minimum = 0;
1909
+ maximum = 1;
1910
+ }
1911
+ else if (peek('{')) {
1912
+ int[] bounds = consumeBounds();
1913
+ minimum = bounds[0];
1914
+ maximum = bounds[1];
1915
+ }
1916
+ else {
1917
+ return atom;
1918
+ }
1919
+ if (atom.containsRepeat) {
1920
+ throw new RequestFailure(
1921
+ "invalid_request",
1922
+ "Nested regex repetitions are not supported"
1923
+ );
1924
+ }
1925
+ if (maximum > 1_000) {
1926
+ throw unboundedRepeat();
1927
+ }
1928
+ return new RegexMetrics(
1929
+ repeatPaths(atom.paths, minimum, maximum),
1930
+ maximum * atom.steps,
1931
+ true
1932
+ );
1933
+ }
1934
+
1935
+ private void consumeCharacterClass() {
1936
+ boolean escaped = false;
1937
+ boolean hasContent = false;
1938
+ while (index < expression.length()) {
1939
+ char item = expression.charAt(index++);
1940
+ if (escaped) {
1941
+ escaped = false;
1942
+ hasContent = true;
1943
+ }
1944
+ else if (item == '\\') {
1945
+ escaped = true;
1946
+ }
1947
+ else if (item == ']' && hasContent) {
1948
+ return;
1949
+ }
1950
+ else {
1951
+ hasContent = true;
1952
+ }
1953
+ }
1954
+ throw invalidRegex();
1955
+ }
1956
+
1957
+ private void consumeEscape() {
1958
+ if (index >= expression.length()) {
1959
+ throw invalidRegex();
1960
+ }
1961
+ char escaped = expression.charAt(index++);
1962
+ if (Character.isDigit(escaped) || escaped == 'k') {
1963
+ throw new RequestFailure(
1964
+ "invalid_request",
1965
+ "Regex lookarounds and backreferences are not supported"
1966
+ );
1967
+ }
1968
+ if (escaped == 'Q' || escaped == 'E' || escaped == 'p' || escaped == 'P') {
1969
+ throw new RequestFailure(
1970
+ "invalid_request",
1971
+ "Regex operation is not supported by the bounded matcher"
1972
+ );
1973
+ }
1974
+ if (escaped == 'x') {
1975
+ consumeHexDigits(2);
1976
+ }
1977
+ else if (escaped == 'u') {
1978
+ consumeHexDigits(4);
1979
+ }
1980
+ }
1981
+
1982
+ private void consumeHexDigits(int count) {
1983
+ if (index + count > expression.length()) {
1984
+ throw invalidRegex();
1985
+ }
1986
+ for (int current = 0; current < count; current += 1) {
1987
+ if (Character.digit(expression.charAt(index + current), 16) < 0) {
1988
+ throw invalidRegex();
1989
+ }
1990
+ }
1991
+ index += count;
1992
+ }
1993
+
1994
+ private int[] consumeBounds() {
1995
+ index += 1;
1996
+ int minimum = consumeDecimal();
1997
+ int maximum = minimum;
1998
+ if (peek(',')) {
1999
+ index += 1;
2000
+ if (peek('}')) {
2001
+ throw unboundedRepeat();
2002
+ }
2003
+ maximum = consumeDecimal();
2004
+ }
2005
+ if (!peek('}') || minimum > maximum) {
2006
+ throw invalidRegex();
2007
+ }
2008
+ index += 1;
2009
+ return new int[] { minimum, maximum };
2010
+ }
2011
+
2012
+ private int consumeDecimal() {
2013
+ int start = index;
2014
+ long value = 0;
2015
+ while (index < expression.length() && Character.isDigit(expression.charAt(index))) {
2016
+ value = value * 10 + Character.digit(expression.charAt(index), 10);
2017
+ if (value > Integer.MAX_VALUE) {
2018
+ throw unboundedRepeat();
2019
+ }
2020
+ index += 1;
2021
+ }
2022
+ if (index == start) {
2023
+ throw invalidRegex();
2024
+ }
2025
+ return (int) value;
2026
+ }
2027
+
2028
+ private boolean peek(char expected) {
2029
+ return index < expression.length() && expression.charAt(index) == expected;
2030
+ }
2031
+
2032
+ private static RegexMetrics leaf() {
2033
+ return new RegexMetrics(1, 1, false);
2034
+ }
2035
+
2036
+ private static long repeatPaths(long childPaths, int minimum, int maximum) {
2037
+ long result = 0;
2038
+ long repeated = 1;
2039
+ for (int count = 0; count <= maximum; count += 1) {
2040
+ if (count >= minimum) {
2041
+ result = checkedPaths(result, repeated, false);
2042
+ }
2043
+ if (count < maximum) {
2044
+ repeated = checkedPaths(repeated, childPaths, true);
2045
+ }
2046
+ }
2047
+ return result;
2048
+ }
2049
+
2050
+ private static long checkedPaths(long left, long right, boolean multiply) {
2051
+ boolean exceeded = multiply
2052
+ ? right != 0 && left > MAX_REGEX_BACKTRACKING_PATHS / right
2053
+ : left > MAX_REGEX_BACKTRACKING_PATHS - right;
2054
+ long result = multiply ? left * right : left + right;
2055
+ if (exceeded || result > MAX_REGEX_BACKTRACKING_PATHS) {
2056
+ throw new RequestFailure(
2057
+ "invalid_request",
2058
+ "Regex exceeds the 10000-path backtracking budget"
2059
+ );
2060
+ }
2061
+ return result;
2062
+ }
2063
+
2064
+ private static RequestFailure unboundedRepeat() {
2065
+ return new RequestFailure(
2066
+ "invalid_request",
2067
+ "Unbounded or excessive regex repetitions are not supported"
2068
+ );
2069
+ }
2070
+
2071
+ private static RequestFailure invalidRegex() {
2072
+ return new RequestFailure("invalid_request", "Invalid regex pattern");
2073
+ }
2074
+ }
2075
+ }