Files
reverse-engineering-anything/bridge/ghidra/ReaGhidraBridge.java
T
morluto bef234d6ce fix(ghidra): preserve Unicode boundaries without breaking sessions
Validate native annotation text before mutation and escape surrogate code units in Java bridge JSON replies. Reject unsupported text with field and index diagnostics while preserving supported Unicode and line endings.

Probe real CLI/MCP annotation, transport, mapping, rollback and cleanup behavior; replace seven redundant unit tests with E2E and SDK boundary coverage.
2026-10-09 02:50:33 +08:00

3308 lines
154 KiB
Java

// REA's packaged, read-only Ghidra headless bridge.
// @category REA
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.channels.Channels;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import ghidra.app.util.headless.HeadlessScript;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileOptions;
import ghidra.app.decompiler.DecompileResults;
import ghidra.app.decompiler.DecompiledFunction;
import ghidra.app.decompiler.ClangCaseToken;
import ghidra.app.decompiler.ClangNode;
import ghidra.framework.Application;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.address.AddressRangeIterator;
import ghidra.program.model.block.BasicBlockModel;
import ghidra.program.model.block.CodeBlock;
import ghidra.program.model.block.CodeBlockIterator;
import ghidra.program.model.block.CodeBlockReference;
import ghidra.program.model.block.CodeBlockReferenceIterator;
import ghidra.program.model.data.StringDataInstance;
import ghidra.program.model.data.DataType;
import ghidra.program.model.lang.Register;
import ghidra.program.model.scalar.Scalar;
import ghidra.program.model.listing.CommentType;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.DataIterator;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.listing.InstructionIterator;
import ghidra.program.model.listing.Variable;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.mem.MemoryBlockSourceInfo;
import ghidra.program.database.mem.FileBytes;
import ghidra.program.model.address.SegmentedAddress;
import ghidra.program.model.reloc.Relocation;
import java.io.ByteArrayOutputStream;
import ghidra.program.model.symbol.RefType;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.ReferenceIterator;
import ghidra.program.model.symbol.SourceType;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolIterator;
import ghidra.program.model.pcode.FunctionPrototype;
import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.pcode.HighParam;
import ghidra.program.model.pcode.HighSymbol;
import ghidra.program.model.pcode.JumpTable;
import ghidra.program.model.pcode.PcodeOp;
import ghidra.program.model.pcode.PcodeOpAST;
import ghidra.program.model.pcode.PcodeBlock;
import ghidra.program.model.pcode.PcodeBlockBasic;
import ghidra.program.model.pcode.Varnode;
import ghidra.program.model.pcode.VarnodeAST;
public final class ReaGhidraBridge extends HeadlessScript {
private static final int MAX_HIGH_PCODE_OPS = 3000;
private static final int MAX_HIGH_PCODE_INPUTS_PER_OP = 64;
private static final int MAX_HIGH_PCODE_DEF_USE_EDGES = 12000;
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private static final Set<String> DESCRIPTOR_KEYS = Set.of(
"transport",
"endpoint_path",
"token",
"run_id",
"target_sha256",
"provider_version",
"profile_digest"
);
private static final Set<String> REQUEST_KEYS = Set.of(
"id",
"token",
"method",
"params"
);
private static final String[] CAPABILITIES = {
"annotate_native_function",
"inspect_native_load_image",
"read_bytes",
"address_to_file_offset",
"ping",
"shutdown",
"address_name",
"list_documents",
"list_names",
"list_procedures",
"list_segments",
"list_strings",
"procedure_address",
"resolve_containing_procedure",
"search_procedures",
"search_strings",
"analyze_function",
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_info",
"procedure_pseudo_code",
"read_function_instructions",
"inspect_native_instruction",
"inspect_native_data_type",
"resolve_native_call_targets",
"procedure_references",
"xrefs"
};
private List<InventoryItem> nameInventory;
private List<FunctionEntry> procedureInventory;
private List<InventoryItem> stringInventory;
private DecompInterface decompiler;
private final ReaGhidraExtensions analysisExtensions = new ReaGhidraExtensions();
private static AddressSpace sessionDefaultAddressSpace;
@Override
public void run() throws Exception {
String[] arguments = getScriptArgs();
if (arguments.length != 1) {
throw new IllegalArgumentException("REA bridge requires one session descriptor");
}
Path descriptorPath = Path.of(arguments[0]);
SessionDescriptor descriptor = readDescriptor(descriptorPath);
// Windows retains a native immutable bearer descriptor lease until
// runtime_close; POSIX removes it after this read. Normal cleanup removes
// the Windows file, while abrupt owner death can leave private residue.
// Its protected runtime DACL also protects the bearer token while the
// bridge is running. POSIX keeps its existing consume-and-delete flow.
if (!isWindowsHost()) {
Files.deleteIfExists(descriptorPath);
}
if (currentProgram == null) {
throw new IllegalStateException("REA bridge requires an imported program");
}
sessionDefaultAddressSpace =
currentProgram.getAddressFactory().getDefaultAddressSpace();
if (currentProgram.getExecutableFormat().equals("Raw Binary") &&
!currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
.getBoolean("REA DOS COM prepared", false)) {
throw new IllegalStateException("REA DOS COM entry/context preparation failed");
}
if (!Application.getApplicationVersion().equals(descriptor.providerVersion)) {
throw new IllegalStateException("Ghidra provider version does not match the session");
}
String importedSha256 = currentProgram.getExecutableSHA256();
if (importedSha256 == null ||
!constantTimeEquals(
importedSha256.toLowerCase(Locale.ROOT),
descriptor.targetSha256
)) {
throw new IllegalStateException(
"Ghidra imported-byte digest does not match the admitted target"
);
}
// GhidraScript wraps run() in a transaction. End it before serving so
// each edit owns a real outer transaction and can roll back immediately.
end(true);
try {
if (!descriptor.transport.equals("unix-socket") && descriptor.analysisExtensions.size() > 0)
throw new IOException("Windows Ghidra P0 does not admit metadata recovery or database mutation.");
analysisExtensions.analyze(descriptor.analysisExtensions, currentProgram, monitor);
initializeDecompiler();
serve(descriptor);
}
finally {
if (decompiler != null) {
decompiler.dispose();
decompiler = null;
}
analysisExtensions.close();
}
}
private void serve(SessionDescriptor descriptor) throws Exception {
if (descriptor.transport.equals("unix-socket")) {
serveUnixSocket(descriptor);
return;
}
if (descriptor.transport.equals("authenticated-loopback-tcp")) {
serveLoopbackTcp(descriptor);
return;
}
throw new IllegalArgumentException("REA bridge transport is invalid");
}
private void serveUnixSocket(SessionDescriptor descriptor) throws Exception {
Path socketPath = Path.of(descriptor.endpointPath);
Files.deleteIfExists(socketPath);
try (ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {
server.bind(UnixDomainSocketAddress.of(socketPath));
Files.setPosixFilePermissions(socketPath, PosixFilePermissions.fromString("rw-------"));
acceptClient(server, descriptor);
}
finally {
Files.deleteIfExists(socketPath);
}
}
private void serveLoopbackTcp(SessionDescriptor descriptor) throws Exception {
Path endpointPath = Path.of(descriptor.endpointPath);
Path pendingPath = endpointPath.resolveSibling(endpointPath.getFileName() + ".pending");
Files.deleteIfExists(endpointPath);
Files.deleteIfExists(pendingPath);
try (ServerSocketChannel server = ServerSocketChannel.open()) {
server.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0));
InetSocketAddress address = (InetSocketAddress) server.getLocalAddress();
JsonObject endpoint = new JsonObject();
endpoint.addProperty("host", "127.0.0.1");
endpoint.addProperty("port", address.getPort());
// The Windows native reader refuses a file while a writer still
// owns it. Closing CREATE_NEW publishes the complete record without
// requesting rename rights on the retained private directory guard.
Files.writeString(
isWindowsHost() ? endpointPath : pendingPath,
GSON.toJson(endpoint) + "\n",
StandardCharsets.UTF_8,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.WRITE
);
if (!isWindowsHost()) {
try {
Files.move(pendingPath, endpointPath, StandardCopyOption.ATOMIC_MOVE);
}
catch (AtomicMoveNotSupportedException exception) {
Files.move(pendingPath, endpointPath);
}
}
acceptClient(server, descriptor);
}
finally {
Files.deleteIfExists(pendingPath);
Files.deleteIfExists(endpointPath);
}
}
private static boolean isWindowsHost() {
return System.getProperty("os.name", "").startsWith("Windows");
}
private void acceptClient(
ServerSocketChannel server,
SessionDescriptor descriptor) throws Exception {
try (SocketChannel client = server.accept();
BufferedReader reader = new BufferedReader(
Channels.newReader(client, StandardCharsets.UTF_8)
);
BufferedWriter writer = new BufferedWriter(
Channels.newWriter(client, StandardCharsets.UTF_8)
)) {
serveClient(descriptor, reader, writer);
}
}
private void serveClient(
SessionDescriptor descriptor,
BufferedReader reader,
BufferedWriter writer) throws Exception {
while (true) {
String line = reader.readLine();
if (line == null) {
return;
}
Request request;
try {
request = parseRequest(line, descriptor.token);
}
catch (RuntimeException exception) {
writeFailure(writer, 1, "invalid_request", "Bridge request is invalid");
return;
}
try {
if (request.method.equals("shutdown")) {
requireKeys(request.params, Set.of());
JsonObject result = new JsonObject();
result.addProperty("shutdown", true);
result.addProperty("project_ephemeral", true);
writeSuccess(writer, request.id, result);
return;
}
writeSuccess(writer, request.id, handleRequest(request, descriptor));
}
catch (RequestFailure failure) {
writeFailure(writer, request.id, failure.code, failure.getMessage());
}
catch (Exception exception) {
writeFailure(
writer,
request.id,
"bridge_exception",
exception.getClass().getSimpleName() + ": " + safeMessage(exception)
);
}
}
}
private JsonElement handleRequest(Request request, SessionDescriptor descriptor)
throws Exception {
if (request.method.equals("ping")) {
requireKeys(request.params, Set.of());
return sessionInfo(descriptor);
}
if (analysisTimeoutOccurred()) {
throw new RequestFailure(
"analysis_incomplete",
"Ghidra auto-analysis did not complete for this Program"
);
}
return switch (request.method) {
case "inspect_native_load_image" -> inspectNativeLoadImage(request.params);
case "read_bytes" -> readMemoryBytes(request.params);
case "address_to_file_offset" -> addressToFileOffset(request.params);
case "address_name" -> addressName(request.params);
case "list_documents" -> listDocuments(request.params);
case "list_names" -> listNames(request.params);
case "list_procedures" -> listProcedures(request.params);
case "list_segments" -> listSegments(request.params);
case "list_strings" -> listStrings(request.params);
case "procedure_address" -> procedureAddress(request.params);
case "procedure_assembly" -> procedureAssembly(request.params);
case "procedure_callees" -> procedureCalls(request.params, false);
case "procedure_callers" -> procedureCalls(request.params, true);
case "procedure_info" -> procedureInfo(request.params);
case "procedure_pseudo_code" -> procedurePseudocode(request.params);
case "read_function_instructions" -> readFunctionInstructions(request.params);
case "procedure_references" -> procedureReferences(request.params);
case "resolve_containing_procedure" -> containingProcedure(request.params);
case "search_procedures" -> search(request.params, true);
case "search_strings" -> search(request.params, false);
case "inspect_native_data_type" -> inspectNativeDataType(request.params);
case "inspect_native_instruction" -> inspectNativeInstruction(request.params);
case "resolve_native_call_targets" -> resolveNativeCallTargets(request.params);
case "xrefs" -> xrefs(request.params);
case "analyze_function" -> analyzeFunction(request.params);
case "annotate_native_function" -> {
if (!descriptor.transport.equals("unix-socket"))
throw new RequestFailure("method_unavailable", "Windows P0 does not admit database mutation");
yield annotateNativeFunction(request.params);
}
default -> throw new RequestFailure(
"method_unavailable",
"Bridge method is unavailable"
);
};
}
private JsonObject sessionInfo(SessionDescriptor descriptor) throws Exception {
boolean timedOut = analysisTimeoutOccurred();
JsonObject provider = new JsonObject();
provider.addProperty("id", "ghidra");
provider.addProperty("version", Application.getApplicationVersion());
JsonObject target = new JsonObject();
target.addProperty("name", currentProgram.getName());
target.addProperty("language_id", currentProgram.getLanguageID().getIdAsString());
target.addProperty(
"compiler_spec_id",
currentProgram.getCompilerSpec().getCompilerSpecID().getIdAsString()
);
target.addProperty("image_base", canonicalAddress(currentProgram.getImageBase()));
target.addProperty(
"default_address_space",
currentProgram.getAddressFactory().getDefaultAddressSpace().getName()
);
target.addProperty("sha256", descriptor.targetSha256);
JsonObject result = new JsonObject();
result.addProperty("name", "REA Ghidra bridge");
result.addProperty("run_id", descriptor.runId);
result.addProperty("profile_digest", descriptor.profileDigest);
result.add("provider", provider);
boolean readOnly = !descriptor.transport.equals("unix-socket");
result.addProperty("read_only", readOnly);
result.addProperty("analysis_complete", !timedOut);
result.addProperty("analysis_timed_out", timedOut);
result.add("capabilities", GSON.toJsonTree(
java.util.Arrays.stream(CAPABILITIES)
.filter(value -> !readOnly || !value.equals("annotate_native_function"))
.toArray(String[]::new)
));
result.add("target", target);
JsonArray extensions = analysisExtensions.reports();
if (extensions.size() > 0) result.add("analysis_extensions", extensions);
return result;
}
private static String bytesHex(byte[] bytes) {
return java.util.HexFormat.of().formatHex(bytes);
}
private String hashFileBytes(FileBytes source, boolean modified) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] chunk = new byte[65536];
for (long offset = 0; offset < source.getSize();) {
monitor.checkCancelled();
int length = (int) Math.min(chunk.length, source.getSize() - offset);
int read = modified
? source.getModifiedBytes(offset, chunk, 0, length)
: source.getOriginalBytes(offset, chunk, 0, length);
if (read != length) throw new IOException("Incomplete FileBytes read at " + offset);
digest.update(chunk, 0, read);
offset += read;
}
return bytesHex(digest.digest());
}
private String hashMemory(Address start, long length) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] chunk = new byte[65536];
for (long offset = 0; offset < length;) {
monitor.checkCancelled();
int count = (int) Math.min(chunk.length, length - offset);
int read = currentProgram.getMemory().getBytes(start.add(offset), chunk, 0, count);
if (read != count) throw new IOException("Incomplete memory read at " + start.add(offset));
digest.update(chunk, 0, read);
offset += read;
}
return bytesHex(digest.digest());
}
private JsonObject inspectNativeLoadImage(JsonObject params) throws Exception {
requireKeys(params, Set.of());
JsonObject result = new JsonObject();
JsonArray recovery = analysisExtensions.inspectImage();
if (recovery.size() > 0) result.add("metadata_recovery", recovery);
result.addProperty("executable_format", currentProgram.getExecutableFormat());
result.addProperty("language_id", currentProgram.getLanguageID().getIdAsString());
result.addProperty("compiler_spec_id", currentProgram.getCompilerSpec().getCompilerSpecID().getIdAsString());
result.addProperty("image_base", canonicalAddress(currentProgram.getImageBase()));
result.addProperty("default_address_space", sessionDefaultAddressSpace.getName());
List<FileBytes> files = currentProgram.getMemory().getAllFileBytes();
JsonArray sources = new JsonArray();
for (FileBytes file : files) {
JsonObject source = new JsonObject();
source.addProperty("name", file.getFilename());
source.addProperty("size", file.getSize());
source.addProperty("original_sha256", hashFileBytes(file, false));
source.addProperty("modified_sha256", hashFileBytes(file, true));
sources.add(source);
}
result.add("source_files", sources);
JsonArray mappings = new JsonArray();
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
for (MemoryBlockSourceInfo source : block.getSourceInfos()) {
monitor.checkCancelled();
JsonObject mapping = new JsonObject();
mapping.addProperty("block", block.getName());
mapping.addProperty("start", canonicalAddress(source.getMinAddress()));
mapping.addProperty("end", canonicalAddress(source.getMaxAddress()));
mapping.addProperty("address_space", source.getMinAddress().getAddressSpace().getName());
mapping.addProperty("initialized", block.isInitialized());
mapping.addProperty("loaded", block.isLoaded());
mapping.addProperty("overlay", block.isOverlay());
mapping.addProperty("length", source.getLength());
FileBytes backing = source.getFileBytes().orElse(null);
int sourceIndex = backing == null ? -1 : files.indexOf(backing);
if (sourceIndex < 0) mapping.add("source_file_index", JsonNull.INSTANCE);
else mapping.addProperty("source_file_index", sourceIndex);
long offset = source.getFileBytesOffset();
if (offset < 0) mapping.add("file_offset", JsonNull.INSTANCE);
else mapping.addProperty("file_offset", offset);
if (block.isInitialized()) mapping.addProperty("sha256", hashMemory(source.getMinAddress(), source.getLength()));
else mapping.add("sha256", JsonNull.INSTANCE);
mappings.add(mapping);
}
}
result.add("mappings", mappings);
JsonArray relocations = new JsonArray();
Iterator<Relocation> iterator = currentProgram.getRelocationTable().getRelocations();
while (iterator.hasNext()) {
monitor.checkCancelled();
Relocation relocation = iterator.next();
Address location = relocation.getAddress();
JsonObject row = new JsonObject();
row.addProperty("address", canonicalAddress(location));
if (location instanceof SegmentedAddress segmented) {
row.addProperty("segment", segmented.getSegment());
row.addProperty("offset", segmented.getSegmentOffset());
} else {
row.add("segment", JsonNull.INSTANCE);
row.add("offset", JsonNull.INSTANCE);
}
row.addProperty("status", relocation.getStatus().toString());
row.addProperty("type", relocation.getType());
row.add("values", GSON.toJsonTree(relocation.getValues() == null ? new long[0] : relocation.getValues()));
byte[] original = relocation.getBytes();
row.add("original_bytes_hex", original == null ? JsonNull.INSTANCE : GSON.toJsonTree(bytesHex(original)));
if (original != null && original.length > 0) {
byte[] memory = new byte[original.length];
int read = currentProgram.getMemory().getBytes(location, memory);
row.add("memory_bytes_hex", read == memory.length ? GSON.toJsonTree(bytesHex(memory)) : JsonNull.INSTANCE);
} else row.add("memory_bytes_hex", JsonNull.INSTANCE);
relocations.add(row);
}
result.add("relocations", relocations);
List<Address> entries = new ArrayList<>();
var entryIterator = currentProgram.getSymbolTable().getExternalEntryPointIterator();
while (entryIterator.hasNext()) entries.add(entryIterator.next());
entries.sort(Address::compareTo);
JsonArray entryPoints = new JsonArray();
for (Address entry : entries) entryPoints.add(canonicalAddress(entry));
result.add("entry_points", entryPoints);
JsonArray contexts = new JsonArray();
for (Address entry : entries) {
JsonObject context = new JsonObject();
context.addProperty("address", canonicalAddress(entry));
JsonArray registers = new JsonArray();
for (String name : new String[] { "CS", "DS", "ES", "SS" }) {
Register register = currentProgram.getRegister(name);
if (register == null) continue;
JsonObject row = new JsonObject();
row.addProperty("name", name);
BigInteger value = currentProgram.getProgramContext().getValue(register, entry, false);
row.add("value_hex", value == null ? JsonNull.INSTANCE : GSON.toJsonTree("0x" + value.toString(16)));
registers.add(row);
}
context.add("registers", registers);
contexts.add(context);
}
result.add("entry_context", contexts);
return result;
}
private JsonObject readMemoryBytes(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address", "length"));
requireDocument(params);
Address start = requireAddress(params, "address");
JsonElement requested = params.get("length");
long length;
try {
if (requested == null || !requested.isJsonPrimitive() || !requested.getAsJsonPrimitive().isNumber())
throw new IllegalArgumentException();
length = requested.getAsBigDecimal().longValueExact();
if (length < 1 || length > 9007199254740991L) throw new IllegalArgumentException();
} catch (RuntimeException failure) {
throw new RequestFailure("invalid_request", "Byte length must be a positive safe integer");
}
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
byte[] chunk = new byte[65536];
for (long offset = 0; offset < length;) {
monitor.checkCancelled();
Address at;
try { at = start.addNoWrap(offset); }
catch (Exception endOfSpace) { break; }
MemoryBlock block = currentProgram.getMemory().getBlock(at);
if (block == null || !block.isInitialized()) break;
int count = (int) Math.min(chunk.length, Math.min(length - offset, block.getEnd().subtract(at) + 1));
int read = currentProgram.getMemory().getBytes(at, chunk, 0, count);
bytes.write(chunk, 0, read);
if (read != count) break;
offset += read;
}
JsonObject result = new JsonObject();
result.addProperty("address", canonicalAddress(start));
result.addProperty("requested_bytes", length);
result.addProperty("returned_bytes", bytes.size());
result.addProperty("bytes_hex", bytesHex(bytes.toByteArray()));
result.addProperty("complete", bytes.size() == length);
return result;
}
private JsonObject addressToFileOffset(JsonObject params) {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
Address address = requireAddress(params, "address");
MemoryBlock block = currentProgram.getMemory().getBlock(address);
if (block == null || !block.isInitialized())
throw new RequestFailure("invalid_request", "Address has no initialized file-backed memory: " + canonicalAddress(address));
Long fileOffset = null;
int mappings = 0;
for (MemoryBlockSourceInfo source : block.getSourceInfos()) {
if (!source.contains(address) || source.getFileBytes().isEmpty()) continue;
long offset = source.getFileBytesOffset(address);
if (offset < 0) continue;
fileOffset = offset;
mappings++;
}
if (mappings != 1)
throw new RequestFailure("invalid_request", "Address has " + mappings + " authoritative source mappings: " + canonicalAddress(address));
JsonObject result = new JsonObject();
result.addProperty("address", canonicalAddress(address));
result.addProperty("file_offset", fileOffset);
return result;
}
private void initializeDecompiler() {
DecompileOptions options = new DecompileOptions();
options.setMaxInstructions(Integer.MAX_VALUE);
decompiler = new DecompInterface();
decompiler.setOptions(options);
decompiler.toggleCCode(true);
decompiler.toggleSyntaxTree(true);
decompiler.toggleJumpLoads(true);
if (!decompiler.openProgram(currentProgram)) {
throw new IllegalStateException("Ghidra decompiler could not open the imported Program");
}
}
private JsonElement procedurePseudocode(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "procedure"));
requireDocument(params);
String value = decompiledText(
decompile(resolveProcedure(requireString(params, "procedure")))
);
return value == null ? JsonNull.INSTANCE : GSON.toJsonTree(value);
}
private JsonElement procedureAssembly(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "procedure"));
requireDocument(params);
Function function = resolveProcedure(requireString(params, "procedure"));
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
return GSON.toJsonTree(renderAssembly(scan.instructions));
}
private JsonObject readFunctionInstructions(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "procedure"));
requireDocument(params);
Function function = resolveProcedure(requireString(params, "procedure"));
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
JsonArray items = new JsonArray();
for (String line : renderAssemblyLines(scan.instructions)) {
items.add(line);
}
JsonArray limitations = new JsonArray();
limitations.add("Instruction text and ordering are Ghidra-specific representations.");
limitations.add(
"The fast path does not invoke the decompiler or scan whole-program names and strings."
);
JsonObject result = new JsonObject();
result.add("procedure", procedureIdentity(function));
result.add("instructions", items);
result.add("limitations", limitations);
return result;
}
private JsonArray procedureCalls(JsonObject params, boolean callers) throws Exception {
requireKeys(params, Set.of("document", "procedure"));
requireDocument(params);
Function function = resolveProcedure(requireString(params, "procedure"));
Set<Function> observed = callers
? function.getCallingFunctions(monitor)
: function.getCalledFunctions(monitor);
List<Function> ordered = new ArrayList<>(observed);
ordered.sort(Comparator.comparing(Function::getEntryPoint));
JsonArray result = new JsonArray();
for (Function item : ordered) {
result.add(canonicalAddress(item.getEntryPoint()));
}
return result;
}
private JsonObject procedureInfo(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "procedure"));
requireDocument(params);
Function function = resolveProcedure(requireString(params, "procedure"));
JsonObject result = new JsonObject();
result.addProperty("name", procedureName(function));
result.addProperty("entrypoint", canonicalAddress(function.getEntryPoint()));
result.addProperty("basicblock_count", basicBlocks(function).size());
JsonObject body = functionBody(function);
result.addProperty("length", body.get("total_bytes").getAsLong());
result.add("body", body);
result.addProperty("signature", function.getPrototypeString(false, true));
result.add("locals", functionLocals(function));
result.add("classification", functionClassification(function));
return result;
}
private JsonObject procedureReferences(JsonObject params) throws Exception {
requireKeys(
params,
Set.of(
"document",
"procedure",
"direction"
)
);
requireDocument(params);
Function function = resolveProcedure(requireString(params, "procedure"));
String direction = requireString(params, "direction");
if (!direction.equals("incoming") && !direction.equals("outgoing")) {
throw new RequestFailure("invalid_request", "direction must be incoming or outgoing");
}
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
List<Reference> references = collectReferences(scan.instructions, direction);
JsonArray edges = new JsonArray();
for (Reference reference : references) {
edges.add(referenceEdge(reference));
}
JsonObject result = new JsonObject();
result.add("procedure", procedureIdentity(function));
result.addProperty("direction", direction);
result.add("references", edges);
result.addProperty("reference_kinds_available", true);
JsonArray unresolvedCalls = new JsonArray();
if (direction.equals("outgoing")) for (Instruction instruction : scan.instructions) {
if (!instruction.getFlowType().isCall()) continue;
boolean resolved = false;
for (Reference reference : instruction.getReferencesFrom()) if (reference.getReferenceType().isCall()) resolved = true;
if (!resolved) {
JsonObject site = new JsonObject();
site.addProperty("address", canonicalAddress(instruction.getAddress()));
site.addProperty("reason", "Instruction decoder identifies a call but ReferenceManager supplies no call destination");
unresolvedCalls.add(site);
}
}
result.add("unresolved_calls", unresolvedCalls);
return result;
}
private JsonObject inspectNativeDataType(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "type", "address"));
requireDocument(params);
boolean byType = params.has("type") && !params.get("type").isJsonNull();
boolean byAddress = params.has("address") && !params.get("address").isJsonNull();
if (byType == byAddress) throw new RequestFailure("invalid_request", "Supply exactly one of type or address");
DataType type;
Address address = null;
if (byType) type = currentProgram.getDataTypeManager().getDataType(requireString(params, "type"));
else {
address = requireAddress(params, "address");
Data data = currentProgram.getListing().getDefinedDataAt(address);
type = data == null ? null : data.getDataType();
}
JsonObject result = new JsonObject();
result.addProperty("status", type == null ? "unavailable" : "available");
result.add("reason", type == null ? GSON.toJsonTree("No exact database type or defined typed data exists at the selected identity") : JsonNull.INSTANCE);
for (String key : List.of("id", "name", "source_archive", "size_bytes", "alignment_bytes", "packing_enabled", "referenced_type", "array_count", "array_stride_bytes")) result.add(key, JsonNull.INSTANCE);
result.add("address", address == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(address)));
result.addProperty("source", "analysis-database");
result.addProperty("kind", "unavailable");
JsonArray fields = new JsonArray();
JsonArray members = new JsonArray();
int totalFields = 0;
if (type != null) {
result.addProperty("id", type.getPathName());
result.addProperty("name", type.getName());
if (type.getSourceArchive() != null) result.addProperty("source_archive", type.getSourceArchive().getName());
if (type.getLength() >= 0) result.addProperty("size_bytes", type.getLength());
if (type.getAlignment() > 0) result.addProperty("alignment_bytes", type.getAlignment());
String kind = type instanceof ghidra.program.model.data.Structure ? "struct" : type instanceof ghidra.program.model.data.Union ? "union" : type instanceof ghidra.program.model.data.Enum ? "enum" : type instanceof ghidra.program.model.data.Pointer ? "pointer" : type instanceof ghidra.program.model.data.Array ? "array" : type instanceof ghidra.program.model.data.TypeDef ? "typedef" : type instanceof ghidra.program.model.data.BuiltInDataType ? "scalar" : "unsupported";
result.addProperty("kind", kind);
if (type instanceof ghidra.program.model.data.Composite composite) {
result.addProperty("packing_enabled", composite.isPackingEnabled());
ghidra.program.model.data.DataTypeComponent[] components = composite.getComponents();
totalFields = components.length;
for (int index = 0; index < Math.min(components.length, 20_000); index++) {
monitor.checkCancelled();
ghidra.program.model.data.DataTypeComponent component = components[index];
JsonObject field = new JsonObject();
field.addProperty("ordinal", component.getOrdinal());
field.add("name", component.getFieldName() == null ? JsonNull.INSTANCE : GSON.toJsonTree(component.getFieldName()));
field.addProperty("offset_bytes", component.getOffset());
field.add("size_bytes", component.getLength() < 0 ? JsonNull.INSTANCE : GSON.toJsonTree(component.getLength()));
field.addProperty("type_id", component.getDataType().getPathName());
field.add("bit_size", JsonNull.INSTANCE);
field.add("bit_offset", JsonNull.INSTANCE);
if (component.getDataType() instanceof ghidra.program.model.data.BitFieldDataType bitfield) {
field.addProperty("bit_size", bitfield.getBitSize());
field.addProperty("bit_offset", bitfield.getBitOffset());
}
field.addProperty("status", "observed");
field.addProperty("flexible_tail", "unknown");
fields.add(field);
}
} else if (type instanceof ghidra.program.model.data.Enum enumeration) {
String[] names = enumeration.getNames();
totalFields = names.length;
java.util.Arrays.sort(names);
for (int index = 0; index < Math.min(names.length, 20_000); index++) {
monitor.checkCancelled();
JsonObject member = new JsonObject();
member.addProperty("name", names[index]);
member.addProperty("value", Long.toString(enumeration.getValue(names[index])));
members.add(member);
}
} else if (type instanceof ghidra.program.model.data.Pointer pointer) {
if (pointer.getDataType() != null) result.addProperty("referenced_type", pointer.getDataType().getPathName());
} else if (type instanceof ghidra.program.model.data.Array array) {
result.addProperty("referenced_type", array.getDataType().getPathName());
result.addProperty("array_count", array.getNumElements());
result.addProperty("array_stride_bytes", array.getElementLength());
} else if (type instanceof ghidra.program.model.data.TypeDef alias) {
result.addProperty("referenced_type", alias.getDataType().getPathName());
}
}
result.add("fields", fields);
result.add("members", members);
result.addProperty("total_fields", totalFields);
result.addProperty("truncated", totalFields > 20_000);
result.add("limitations", GSON.toJsonTree(List.of("Layouts are Ghidra data-type database observations; original debug/source authority and flexible-tail semantics are not inferred. Nested and recursive types retain category-path identities for separate inspection.", "At most 20000 fields or enum members are serialized per type to bound provider output. total_fields and truncated report omissions. Enum values preserve Ghidra's signed long representation.")));
if (type != null) {
JsonObject metadata = analysisExtensions.inspectDataType(currentProgram, type, address, monitor);
if (metadata != null) result.add("metadata_recovery", metadata);
}
return result;
}
private JsonObject instructionReference(Reference reference) {
JsonObject item = new JsonObject();
RefType type = reference.getReferenceType();
item.addProperty("target_address", canonicalAddress(reference.getToAddress()));
item.addProperty("type", type.getName());
item.addProperty("call", type.isCall());
item.addProperty("jump", type.isJump());
item.addProperty("indirect", type.isIndirect());
item.addProperty("computed", type.isComputed());
item.addProperty("operand_index", reference.getOperandIndex());
return item;
}
private JsonObject inspectNativeInstruction(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
Address address = requireAddress(params, "address");
Instruction instruction = currentProgram.getListing().getInstructionAt(address);
Function function = currentProgram.getFunctionManager().getFunctionContaining(address);
JsonObject result = new JsonObject();
result.addProperty("address", canonicalAddress(address));
result.add("procedure", function == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(function.getEntryPoint())));
result.addProperty("architecture", currentProgram.getLanguageID().getIdAsString());
result.addProperty("mode", currentProgram.getLanguage().getLanguageDescription().getVariant());
JsonArray operands = new JsonArray();
JsonArray references = new JsonArray();
JsonArray destinations = new JsonArray();
JsonObject flow = new JsonObject();
flow.addProperty("kind", "unavailable");
flow.addProperty("conditional", false);
flow.addProperty("computed", false);
if (instruction == null) {
result.addProperty("status", !currentProgram.getMemory().contains(address) ? "outside-memory" : currentProgram.getListing().getInstructionContaining(address) != null
? "not-instruction-boundary" : currentProgram.getListing().getDefinedDataContaining(address) != null ? "data" : "undecodable");
for (String key : List.of("bytes", "length", "mnemonic", "raw_disassembly")) result.add(key, JsonNull.INSTANCE);
} else {
result.addProperty("status", "decoded");
result.addProperty("bytes", java.util.HexFormat.of().formatHex(instruction.getBytes()));
result.addProperty("length", instruction.getLength());
result.addProperty("mnemonic", instruction.getMnemonicString());
result.addProperty("raw_disassembly", instruction.toString());
RefType type = instruction.getFlowType();
flow.addProperty("kind", type.isCall() ? "call" : type.isJump() ? "jump" : type.isTerminal() ? "terminal" : "fallthrough");
flow.addProperty("conditional", type.isConditional());
flow.addProperty("computed", type.isComputed());
if (!type.isComputed()) for (Address target : instruction.getFlows()) destinations.add(canonicalAddress(target));
for (Reference reference : instruction.getReferencesFrom()) references.add(instructionReference(reference));
for (int index = 0; index < instruction.getNumOperands(); index++) {
JsonObject operand = new JsonObject();
operand.addProperty("index", index);
operand.addProperty("raw", instruction.getDefaultOperandRepresentation(index));
operand.addProperty("provider_type", instruction.getOperandType(index));
operand.addProperty("memory_addressing", "unavailable");
JsonArray components = new JsonArray();
for (Object object : instruction.getOpObjects(index)) {
JsonObject component = new JsonObject();
component.addProperty("text", object.toString());
component.add("value", JsonNull.INSTANCE);
component.add("bit_width", JsonNull.INSTANCE);
if (object instanceof Register register) {
component.addProperty("kind", "register");
component.addProperty("value", register.getName());
component.addProperty("bit_width", register.getBitLength());
} else if (object instanceof Scalar scalar) {
component.addProperty("kind", "immediate");
component.addProperty("value", "0x" + Long.toUnsignedString(scalar.getUnsignedValue(), 16));
component.addProperty("bit_width", scalar.bitLength());
} else if (object instanceof Address target) {
component.addProperty("kind", "address");
component.addProperty("value", canonicalAddress(target));
} else component.addProperty("kind", "unknown");
components.add(component);
}
operand.add("components", components);
operands.add(operand);
}
}
flow.add("direct_destinations", destinations);
result.add("operands", operands);
result.add("flow", flow);
result.add("references", references);
result.add("limitations", GSON.toJsonTree(List.of("Operand components are Ghidra Listing decoder tokens; effective memory base/index/displacement roles and per-instruction context mode are unavailable. The mode field is the program language variant.", "Terminal flow is not classified as a return without provider evidence.")));
return result;
}
private JsonObject resolveNativeCallTargets(JsonObject params) throws Exception {
JsonObject instruction = inspectNativeInstruction(params);
JsonObject result = new JsonObject();
result.add("call_site", instruction.get("address"));
result.add("procedure", instruction.get("procedure"));
JsonArray targets = new JsonArray();
JsonObject flow = instruction.getAsJsonObject("flow");
boolean decoded = instruction.get("status").getAsString().equals("decoded");
boolean call = flow.get("kind").getAsString().equals("call");
boolean computed = flow.get("computed").getAsBoolean();
Map<String, JsonArray> byTarget = new TreeMap<>();
if (call) for (JsonElement element : instruction.getAsJsonArray("references")) {
JsonObject reference = element.getAsJsonObject();
if (reference.get("call").getAsBoolean())
byTarget.computeIfAbsent(reference.get("target_address").getAsString(), ignored -> new JsonArray()).add(reference);
}
boolean ambiguous = byTarget.size() > 1;
for (Map.Entry<String, JsonArray> entry : byTarget.entrySet()) {
JsonObject target = new JsonObject();
target.addProperty("address", entry.getKey());
Address address = tryParseAddress(entry.getKey());
Function function = address == null ? null : currentProgram.getFunctionManager().getFunctionAt(address);
target.add("procedure", function == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(function.getEntryPoint())));
target.addProperty("status", ambiguous ? "candidate" : computed ? "resolved-indirect" : "direct");
target.addProperty("basis", "provider-reference");
target.add("references", entry.getValue());
targets.add(target);
}
result.addProperty("status", !decoded ? "unavailable" : !call ? "not-call" : ambiguous ? "ambiguous" : targets.size() == 0 ? "unresolved" : computed ? "resolved-indirect" : "direct");
result.addProperty("mechanism", !call ? "unavailable" : computed ? "computed" : "direct");
result.add("targets", targets);
result.add("limitations", GSON.toJsonTree(List.of("Targets are static Ghidra ReferenceManager call-reference observations. Computed calls without call references remain unresolved; a resolved reference is not proof of runtime execution.", "Objective-C, Swift, vtable and closure mechanisms are not identified by this reference-only boundary.")));
return result;
}
private JsonArray xrefs(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
Address address = requireAddress(params, "address");
ReferenceIterator references = currentProgram.getReferenceManager().getReferencesTo(address);
Set<Address> sources = new HashSet<>();
while (references.hasNext()) {
monitor.checkCancelled();
Reference reference = references.next();
if (reference.isEntryPointReference()) {
continue;
}
sources.add(reference.getFromAddress());
}
List<Address> ordered = new ArrayList<>(sources);
ordered.sort(Address::compareTo);
JsonArray result = new JsonArray();
for (Address source : ordered) {
result.add(canonicalAddress(source));
}
return result;
}
private void invalidateAnalysisCaches() {
nameInventory = null;
procedureInventory = null;
stringInventory = null;
if (decompiler != null) decompiler.flushCache();
}
private JsonObject annotationReadback(Function function) {
Address entry = function.getEntryPoint();
JsonObject value = new JsonObject();
value.addProperty("address", canonicalAddress(entry));
value.addProperty("name", procedureName(function));
value.addProperty("comment", currentProgram.getListing().getComment(CommentType.PRE, entry));
value.addProperty("inline_comment", currentProgram.getListing().getComment(CommentType.EOL, entry));
return value;
}
private JsonObject annotateNativeFunction(JsonObject params) throws Exception {
if (!Set.of("procedure", "name", "comment", "inline_comment").containsAll(params.keySet()))
throw new RequestFailure("invalid_request", "Unknown function annotation field");
if (!params.has("name") && !params.has("comment") && !params.has("inline_comment"))
throw new RequestFailure("invalid_request", "Supply at least one annotation change");
Function function = resolveProcedure(requireString(params, "procedure"));
Address entry = function.getEntryPoint();
if (function.isExternal() || !currentProgram.getMemory().contains(entry))
throw new RequestFailure("invalid_request", "Annotations require a local function entry: " + canonicalAddress(entry));
for (String field : List.of("name", "comment", "inline_comment")) {
if (params.has(field)) validateAnnotationText(requireText(params, field), field);
}
int transaction = currentProgram.startTransaction("REA function annotations");
boolean commit = false;
try {
// Validate edits through Ghidra's own setters, including name rules.
// A later invalid name must roll back earlier comment writes.
for (String field : List.of("comment", "inline_comment")) {
if (params.has(field)) {
String text = requireText(params, field);
CommentType type = field.equals("comment") ? CommentType.PRE : CommentType.EOL;
currentProgram.getListing().setComment(entry, type, text.isEmpty() ? null : text);
}
}
if (params.has("name"))
function.setName(requireString(params, "name"), SourceType.USER_DEFINED);
monitor.checkCancelled();
invalidateAnalysisCaches();
JsonObject readback = annotationReadback(function);
for (String field : List.of("name", "comment", "inline_comment")) {
if (!params.has(field)) continue;
String requested = requireText(params, field);
JsonElement measured = readback.get(field);
String expected = !field.equals("name") && requested.isEmpty() ? null : requested;
String observed = field.equals("name") ? function.getName() :
(measured.isJsonNull() ? null : measured.getAsString());
if (!java.util.Objects.equals(expected, observed))
throw new RequestFailure("annotation_readback_mismatch", "Annotation readback differs for " + field + " at " + canonicalAddress(entry));
}
JsonObject query = new JsonObject();
query.addProperty("procedure", canonicalAddress(entry));
JsonObject result = new JsonObject();
result.add("annotations", readback);
result.add("dossier", analyzeFunction(query));
JsonObject effects = new JsonObject();
effects.addProperty("scope", "session-analysis-database");
effects.addProperty("source_bytes_modified", false);
effects.addProperty("persists_after_close", false);
result.add("effects", effects);
monitor.checkCancelled();
commit = true;
return result;
}
catch (ghidra.util.exception.InvalidInputException | ghidra.util.exception.DuplicateNameException exception) {
throw new RequestFailure("invalid_function_name", "Invalid function name at " + canonicalAddress(entry) + ": " + safeMessage(exception));
}
finally {
currentProgram.endTransaction(transaction, commit);
// Rollback also invalidates inventory and decompiler views.
invalidateAnalysisCaches();
}
}
private static String requireText(JsonObject params, String field) {
JsonElement value = params.get(field);
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
throw new RequestFailure("invalid_request", "Annotation " + field + " must be text");
return value.getAsString();
}
private static void validateAnnotationText(String text, String field) {
for (int index = 0; index < text.length(); index++) {
char character = text.charAt(index);
if (character == 0)
throw new RequestFailure("invalid_request", "Annotation " + field + " contains NUL at UTF-16 index " + index + "; Ghidra's decompiler cannot marshal NUL text");
if (Character.isHighSurrogate(character) && index + 1 < text.length() &&
Character.isLowSurrogate(text.charAt(index + 1))) {
index++;
continue;
}
if (Character.isSurrogate(character))
throw new RequestFailure("invalid_request", "Annotation " + field + " contains an unpaired Unicode surrogate at UTF-16 index " + index + "; Ghidra cannot preserve it as annotation text");
}
}
private JsonObject analyzeFunction(JsonObject params) throws Exception {
requireKeys(params, Set.of("procedure"));
Function function = resolveProcedure(requireString(params, "procedure"));
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
DecompileResults decompilation = decompile(function);
String pseudocode = decompiledText(decompilation);
if (pseudocode == null) {
pseudocode = "";
}
List<Reference> incomingReferences = collectReferences(scan.instructions, "incoming")
.stream()
.filter(reference -> !function.getBody().contains(reference.getFromAddress()))
.toList();
List<Reference> outgoingReferences = collectReferences(scan.instructions, "outgoing");
JsonArray incoming = referenceEdges(incomingReferences);
JsonArray outgoing = referenceEdges(outgoingReferences);
JsonArray comments = comments(scan.instructions);
JsonArray callers = procedureIdentities(function.getCallingFunctions(monitor));
JsonArray callees = procedureIdentities(function.getCalledFunctions(monitor));
JsonArray referencedStrings = referencedStrings(outgoingReferences);
JsonArray referencedNames = referencedNames(outgoingReferences);
JsonArray blocks = basicBlockValues(function);
JsonArray assembly = GSON.toJsonTree(renderAssemblyLines(scan.instructions))
.getAsJsonArray();
JsonObject procedure = procedureIdentity(function);
procedure.addProperty("signature", function.getPrototypeString(false, true));
procedure.add("locals", functionLocals(function));
JsonObject result = new JsonObject();
result.add("procedure", procedure);
result.addProperty("pseudocode", pseudocode);
result.add("assembly", assembly);
result.add("comments", comments);
result.add("callers", callers);
result.add("callees", callees);
result.add("incoming_references", incoming);
result.add("outgoing_references", outgoing);
result.add("referenced_strings", referencedStrings);
result.add("referenced_names", referencedNames);
result.add("basic_blocks", blocks);
result.add(
"native_api",
nativeApiBoundary(function, decompilation)
);
result.add("native_value_flow", nativeValueFlow(decompilation));
JsonArray limitations = new JsonArray();
limitations.add(
"Unresolved computed or indirect flows without target addresses are not represented as reference edges."
);
limitations.add(
"Thunk and external classifications are Ghidra FunctionManager observations; they do not resolve targetless calls."
);
limitations.add(
"Pseudocode and assembly are Ghidra-specific representations, not original source or Hopper-equivalent text."
);
limitations.add(
"Synthetic Ghidra entry-point references without actionable memory sources are omitted."
);
if (decompilation != null && decompilation.getErrorMessage() != null &&
!decompilation.getErrorMessage().isBlank()) {
limitations.add("Ghidra decompiler diagnostic: " + decompilation.getErrorMessage());
}
result.add("limitations", limitations);
return result;
}
private JsonObject nativeValueFlow(DecompileResults decompilation) throws Exception {
HighFunction highFunction =
decompilation == null ? null : decompilation.getHighFunction();
if (highFunction == null) {
JsonObject unavailable = new JsonObject();
unavailable.addProperty("available", false);
unavailable.addProperty(
"reason",
"Ghidra has no HighFunction p-code model for this procedure"
);
JsonArray limitations = new JsonArray();
limitations.add(
"No p-code operations or def-use relationships were recovered for this procedure."
);
unavailable.add("limitations", limitations);
return unavailable;
}
List<PcodeOpAST> selected = new ArrayList<>();
int omittedOperationsLowerBound = 0;
Iterator<PcodeOpAST> operations = highFunction.getPcodeOps();
while (operations.hasNext()) {
monitor.checkCancelled();
PcodeOpAST operation = operations.next();
if (selected.size() >= MAX_HIGH_PCODE_OPS) {
omittedOperationsLowerBound = 1;
break;
}
selected.add(operation);
}
Map<PcodeOpAST, String> operationIds = new IdentityHashMap<>();
for (PcodeOpAST operation : selected) {
operationIds.put(operation, pcodeOperationId(operation));
}
JsonArray parameterUses = new JsonArray();
JsonArray parameterValues = new JsonArray();
FunctionPrototype prototype = highFunction.getFunctionPrototype();
for (int ordinal = 0; ordinal < prototype.getNumParams(); ordinal++) {
HighSymbol parameter = prototype.getParam(ordinal);
if (parameter == null) continue;
JsonObject value = new JsonObject();
value.addProperty("ordinal", ordinal);
value.addProperty("name", parameter.getName());
value.addProperty("data_type", parameter.getDataType().getPathName());
parameterValues.add(value);
}
JsonArray operationValues = new JsonArray();
JsonArray defUse = new JsonArray();
JsonArray effects = new JsonArray();
int omittedInputs = 0;
int omittedEdges = 0;
for (PcodeOpAST operation : selected) {
monitor.checkCancelled();
String operationId = operationIds.get(operation);
JsonObject value = new JsonObject();
value.addProperty("id", operationId);
value.addProperty(
"address",
canonicalAddress(operation.getSeqnum().getTarget())
);
value.addProperty("sequence", operation.getSeqnum().getTime());
value.addProperty("opcode", operation.getMnemonic());
value.addProperty("is_dead", operation.isDead());
value.addProperty("block_membership", operation.getParent() == null ? "detached" : "member");
JsonArray inputs = new JsonArray();
int inputCount = operation.getNumInputs();
int capturedInputs = Math.min(inputCount, MAX_HIGH_PCODE_INPUTS_PER_OP);
for (int inputIndex = 0; inputIndex < capturedInputs; inputIndex += 1) {
Varnode input = operation.getInput(inputIndex);
inputs.add(varnodeValue(input));
if (input instanceof VarnodeAST astInput) {
if (astInput.getHigh() instanceof HighParam parameter) {
if (parameterUses.size() >= MAX_HIGH_PCODE_DEF_USE_EDGES) omittedEdges++;
else {
JsonObject use = new JsonObject();
use.addProperty("ordinal", parameter.getSlot());
use.addProperty("use", operationId);
use.addProperty("input_index", inputIndex);
parameterUses.add(use);
}
}
PcodeOp definition = astInput.getDef();
if (definition instanceof PcodeOpAST astDefinition) {
String definitionId = operationIds.get(astDefinition);
if (definitionId == null || defUse.size() >= MAX_HIGH_PCODE_DEF_USE_EDGES) {
omittedEdges += 1;
} else {
JsonObject edge = new JsonObject();
edge.addProperty("definition", definitionId);
edge.addProperty("use", operationId);
edge.addProperty("input_index", inputIndex);
defUse.add(edge);
}
}
}
}
omittedInputs += inputCount - capturedInputs;
value.add("inputs", inputs);
Varnode output = operation.getOutput();
value.add("output", output == null ? JsonNull.INSTANCE : varnodeValue(output));
operationValues.add(value);
addPcodeEffect(effects, operation, operationId);
}
JsonObject result = new JsonObject();
result.addProperty("available", true);
result.addProperty("provenance", "ghidra-high-pcode");
result.add("operations", operationValues);
result.add("def_use", defUse);
result.add("parameters", parameterValues);
result.add("parameter_uses", parameterUses);
result.add("effects", effects);
boolean truncated = omittedOperationsLowerBound > 0 ||
omittedInputs > 0 || omittedEdges > 0;
result.addProperty("truncated", truncated);
result.addProperty(
"omitted_operations_lower_bound",
omittedOperationsLowerBound
);
result.addProperty("known_omitted_inputs", omittedInputs);
result.addProperty("known_omitted_edges", omittedEdges);
JsonArray limitations = new JsonArray();
limitations.add(
"High p-code is decompiler-derived, not original source or runtime behavior. is_dead preserves the Java AST flag; decoded block members can retain that flag, so block_membership separately reports actual syntax-tree membership."
);
limitations.add(
"Def-use links do not cross function boundaries and may omit memory aliasing, call side effects, or unresolved indirect flow."
);
limitations.add(
"Memory read/write effects include stack and temporary memory operations; they do not identify persistent state or business meaning."
);
limitations.add(
"Operation, input, and def-use limits can truncate large functions; inspect truncation, omitted lower bounds, and known omitted counts before drawing conclusions."
);
limitations.add(
"Known omitted-edge counts cover links omitted from retained operations; omitted operations may contain additional uncounted links."
);
result.add("limitations", limitations);
return result;
}
private String pcodeOperationId(PcodeOpAST operation) {
return canonicalAddress(operation.getSeqnum().getTarget()) + "#" +
operation.getSeqnum().getTime();
}
private JsonObject varnodeValue(Varnode varnode) {
JsonObject result = new JsonObject();
String kind = varnode.isConstant()
? "constant"
: varnode.isAddress()
? "address"
: varnode.isRegister()
? "register"
: varnode.isUnique() ? "unique" : "other";
result.addProperty("kind", kind);
result.addProperty("size_bytes", varnode.getSize());
result.add(
"location",
varnode.isConstant()
? JsonNull.INSTANCE
: GSON.toJsonTree(canonicalAddress(varnode.getAddress()))
);
result.add(
"constant_hex",
varnode.isConstant()
? GSON.toJsonTree(Long.toUnsignedString(varnode.getOffset(), 16))
: JsonNull.INSTANCE
);
return result;
}
private void addPcodeEffect(
JsonArray destination,
PcodeOp operation,
String operationId
) {
String kind = null;
Integer operandInput = null;
Integer valueInput = null;
switch (operation.getOpcode()) {
case PcodeOp.LOAD -> {
kind = "memory_read";
operandInput = 1;
}
case PcodeOp.STORE -> {
kind = "memory_write";
operandInput = 1;
valueInput = 2;
}
case PcodeOp.CBRANCH -> {
kind = "conditional_branch";
valueInput = 1;
}
case PcodeOp.BRANCHIND -> {
kind = "indirect_branch";
operandInput = 0;
}
case PcodeOp.CALL -> {
kind = "direct_call";
operandInput = 0;
}
case PcodeOp.CALLIND -> {
kind = "indirect_call";
operandInput = 0;
}
default -> { }
}
if (kind == null) return;
JsonObject effect = new JsonObject();
effect.addProperty("operation", operationId);
effect.addProperty("kind", kind);
effect.add(
"operand_input",
operandInput == null ? JsonNull.INSTANCE : GSON.toJsonTree(operandInput)
);
effect.add(
"value_input",
valueInput == null ? JsonNull.INSTANCE : GSON.toJsonTree(valueInput)
);
destination.add(effect);
}
private JsonElement addressName(JsonObject params) {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
Address address = requireAddress(params, "address");
Symbol symbol = currentProgram.getSymbolTable().getPrimarySymbol(address);
return symbol == null ? JsonNull.INSTANCE : GSON.toJsonTree(symbol.getName(true));
}
private JsonArray listDocuments(JsonObject params) {
requireKeys(params, Set.of());
JsonArray result = new JsonArray();
result.add(currentProgram.getName());
return result;
}
private JsonArray listNames(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
List<InventoryItem> inventory = names();
String requested = optionalString(params, "address");
if (requested != null) {
Address address = parseReaAddress(requested);
inventory = inventory.stream()
.filter(item -> item.address.equals(address))
.toList();
}
return inventory(inventory, "symbol");
}
private JsonArray listProcedures(JsonObject params) throws Exception {
requireKeys(params, Set.of("document"));
requireDocument(params);
List<InventoryItem> inventory = procedures().stream()
.map(FunctionEntry::item)
.toList();
return inventory(inventory, "procedure");
}
private JsonArray listSegments(JsonObject params) {
requireKeys(params, Set.of("document"));
requireDocument(params);
JsonArray result = new JsonArray();
String imageBase = canonicalAddress(currentProgram.getImageBase());
MemoryBlock[] blocks = currentProgram.getMemory().getBlocks();
List<MemoryBlock> ordered = new ArrayList<>(List.of(blocks));
ordered.sort(Comparator.comparing(MemoryBlock::getStart));
for (MemoryBlock block : ordered) {
JsonObject item = memoryRegion(block, imageBase);
item.add("sections", new JsonArray());
result.add(item);
}
return result;
}
private JsonArray listStrings(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
List<InventoryItem> inventory = strings();
String requested = optionalString(params, "address");
if (requested != null) {
Address address = parseReaAddress(requested);
inventory = inventory.stream()
.filter(item -> item.address.equals(address))
.toList();
}
return inventory(inventory, "string");
}
private JsonElement procedureAddress(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "procedure"));
requireDocument(params);
return GSON.toJsonTree(canonicalAddress(resolveProcedure(requireString(params, "procedure")).getEntryPoint()));
}
private JsonObject containingProcedure(JsonObject params) {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
Address query = requireAddress(params, "address");
Function function = currentProgram.getFunctionManager().getFunctionAt(query);
if (function == null && query.isMemoryAddress()) {
function = currentProgram.getFunctionManager().getFunctionContaining(query);
}
JsonObject result = new JsonObject();
result.addProperty("query_address", canonicalAddress(query));
if (function != null) {
result.addProperty("found", true);
result.add("procedure", procedureIdentity(function));
return result;
}
result.addProperty("found", false);
result.add("procedure", JsonNull.INSTANCE);
boolean inside = query.isMemoryAddress() && currentProgram.getMemory().contains(query);
result.addProperty("reason", inside ? "not_in_procedure" : "outside_segments");
return result;
}
private JsonArray search(JsonObject params, boolean procedureSearch) throws Exception {
requireKeys(
params,
Set.of("pattern", "mode", "case_sensitive", "document")
);
requireDocument(params);
String expression = requireString(params, "pattern");
String mode = requireString(params, "mode");
if (!mode.equals("literal") && !mode.equals("regex")) {
throw new RequestFailure("invalid_request", "mode must be literal or regex");
}
boolean caseSensitive = requireBoolean(params, "case_sensitive");
List<InventoryItem> inventory = procedureSearch
? procedures().stream().map(FunctionEntry::item).toList()
: strings();
ValueMatcher matcher = mode.equals("literal")
? literalMatcher(expression, caseSensitive)
: regexMatcher(expression, caseSensitive);
JsonArray items = new JsonArray();
for (InventoryItem item : inventory) {
if (!matcher.matches(item.value)) {
continue;
}
items.add(resultItem(item, null));
}
return items;
}
private List<InventoryItem> names() throws Exception {
if (nameInventory != null) {
return nameInventory;
}
List<InventoryItem> result = new ArrayList<>();
SymbolIterator symbols = currentProgram.getSymbolTable().getAllSymbols(true);
while (symbols.hasNext()) {
monitor.checkCancelled();
Symbol symbol = symbols.next();
Address address = symbol.getAddress();
if (!address.isMemoryAddress() && !address.isExternalAddress()) {
continue;
}
JsonObject facts = new JsonObject();
facts.addProperty("primary", symbol.isPrimary());
facts.addProperty("dynamic", symbol.isDynamic());
facts.addProperty("external", symbol.isExternal());
facts.addProperty("type", normalizedSymbolType(symbol));
facts.addProperty("source", normalizedSource(symbol.getSource()));
result.add(new InventoryItem(address, symbol.getName(true), facts));
}
result.sort(INVENTORY_ORDER);
nameInventory = List.copyOf(result);
return nameInventory;
}
private List<FunctionEntry> procedures() throws Exception {
if (procedureInventory != null) {
return procedureInventory;
}
List<FunctionEntry> result = new ArrayList<>();
Set<Address> seen = new HashSet<>();
appendFunctions(result, seen, currentProgram.getFunctionManager().getFunctions(true));
appendFunctions(result, seen, currentProgram.getFunctionManager().getExternalFunctions());
result.sort((left, right) -> INVENTORY_ORDER.compare(left.item, right.item));
procedureInventory = List.copyOf(result);
return procedureInventory;
}
private void appendFunctions(
List<FunctionEntry> destination,
Set<Address> seen,
FunctionIterator functions) throws Exception {
while (functions.hasNext()) {
monitor.checkCancelled();
Function function = functions.next();
if (!seen.add(function.getEntryPoint())) {
continue;
}
JsonObject facts = new JsonObject();
facts.addProperty("external", function.isExternal());
facts.addProperty("thunk", function.isThunk());
Function target = function.isThunk() ? function.getThunkedFunction(false) : null;
if (target == null) {
facts.add("thunk_target", JsonNull.INSTANCE);
}
else {
facts.addProperty("thunk_target", canonicalAddress(target.getEntryPoint()));
}
Symbol symbol = function.getSymbol();
InventoryItem item = new InventoryItem(
function.getEntryPoint(),
symbol == null ? function.getName() : symbol.getName(true),
facts
);
destination.add(new FunctionEntry(function, item));
}
}
private List<InventoryItem> strings() throws Exception {
if (stringInventory != null) {
return stringInventory;
}
List<InventoryItem> result = new ArrayList<>();
DataIterator dataItems = currentProgram.getListing().getDefinedData(true);
while (dataItems.hasNext()) {
monitor.checkCancelled();
Data data = dataItems.next();
if (!data.hasStringValue()) {
continue;
}
StringDataInstance instance = StringDataInstance.getStringDataInstance(data);
String value = instance.getStringValue();
if (value == null) {
continue;
}
JsonObject facts = new JsonObject();
String charset = instance.getCharsetName();
facts.addProperty("encoding", charset == null || charset.isEmpty() ? "unknown" : charset);
facts.addProperty(
"termination",
instance.isMissingNullTerminator() ? "missing" : "present_or_not_required"
);
facts.addProperty("byte_length", Math.max(0, data.getLength()));
result.add(new InventoryItem(data.getAddress(), value, facts));
}
result.sort(INVENTORY_ORDER);
stringInventory = List.copyOf(result);
return stringInventory;
}
private Function resolveProcedure(String value) throws Exception {
// Explicit address syntax wins; an unprefixed identifier is first an
// exact symbol name, even when every character is a hexadecimal digit.
boolean explicitAddress = value.matches("(?i)^0x[0-9a-f]+$") ||
value.matches("(?i)^(?:[a-z0-9._~-]|%[0-9a-f]{2})+:0x[0-9a-f]+$");
if (!explicitAddress) {
List<Function> matches = new ArrayList<>();
for (FunctionEntry entry : procedures()) {
monitor.checkCancelled();
Function function = entry.function;
Symbol symbol = function.getSymbol();
String qualified = symbol == null ? function.getName() : symbol.getName(true);
if (function.getName().equals(value) || qualified.equals(value)) {
matches.add(function);
}
}
if (matches.size() > 1)
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous: " + value);
if (matches.size() == 1) return matches.get(0);
}
// Retain legacy bare-hex address input only when no symbol matches.
Address address = explicitAddress ? parseReaAddress(value) : tryParseAddress(value);
if (address == null)
throw new RequestFailure("not_found", "Unknown Ghidra procedure name or address: " + value);
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
if (function == null && address.isMemoryAddress())
function = currentProgram.getFunctionManager().getFunctionContaining(address);
if (function == null)
throw new RequestFailure("not_found", "No procedure exists at the requested address: " + value);
return function;
}
private JsonObject nativeApiBoundary(
Function function,
DecompileResults decompilation
) throws Exception {
if (decompilation == null || decompilation.getHighFunction() == null) {
return unavailableNativeApiBoundary(
"Ghidra has no HighFunction model for an external or bodyless procedure"
);
}
HighFunction highFunction = decompilation.getHighFunction();
FunctionPrototype prototype = highFunction.getFunctionPrototype();
String signatureSource = function
.getSignatureSource()
.name()
.toLowerCase(Locale.ROOT);
JsonObject result = new JsonObject();
result.addProperty("available", true);
result.addProperty("provenance", "ghidra-high-function");
result.addProperty("signature_source", signatureSource);
String callingConvention = prototype.getModelName();
result.addProperty(
"calling_convention",
callingConvention == null || callingConvention.isBlank()
? "unknown"
: callingConvention
);
result.add(
"return_type",
inferredBoundaryType(
"return",
null,
null,
prototype.getReturnType(),
prototype.getReturnStorage().toString(),
false,
signatureSource
)
);
JsonArray parameters = new JsonArray();
result.add("parameters", parameters);
JsonArray jumpTables = new JsonArray();
result.add("jump_tables", jumpTables);
JsonObject pseudocode = new JsonObject();
pseudocode.addProperty("classification", "decompiler-generated-non-source");
pseudocode.addProperty("compilable", false);
result.add("pseudocode", pseudocode);
JsonArray artifacts = new JsonArray();
artifacts.add("recovered-signature");
artifacts.add("register-and-stack-variables");
artifacts.add("compiler-generated-control-flow");
artifacts.add("pointer-arithmetic");
artifacts.add("pseudocode");
result.add("decompiler_artifacts", artifacts);
JsonArray limitations = new JsonArray();
limitations.add(
"Types and calling conventions are Ghidra decompiler observations, not source declarations."
);
limitations.add(
"Register variables, compiler-generated blocks, and pointer arithmetic remain decompiler artifacts."
);
limitations.add(
"Pseudocode is neither original source nor guaranteed to compile."
);
result.add("limitations", limitations);
for (int index = 0; index < prototype.getNumParams(); index += 1) {
HighSymbol parameter = prototype.getParam(index);
if (parameter == null) {
continue;
}
parameters.add(
inferredBoundaryType(
"parameter",
index,
parameter.getName(),
parameter.getDataType(),
parameter.getStorage().toString(),
parameter.isTypeLocked(),
signatureSource
)
);
}
JumpTable[] recoveredJumpTables = highFunction.getJumpTables();
List<ClangNode> caseTokens = new ArrayList<>();
if (decompilation.getCCodeMarkup() != null) {
decompilation.getCCodeMarkup().flatten(caseTokens);
}
for (int index = 0; index < recoveredJumpTables.length; index += 1) {
jumpTables.add(inferredJumpTable(recoveredJumpTables[index], caseTokens));
}
return result;
}
private static JsonObject unavailableNativeApiBoundary(String reason) {
JsonObject unavailable = new JsonObject();
unavailable.addProperty("available", false);
unavailable.addProperty("reason", reason);
JsonArray unknowns = new JsonArray();
unknowns.add("Return and parameter boundary types are unavailable.");
unknowns.add("Jump-table mappings are unavailable.");
unavailable.add("residual_unknowns", unknowns);
return unavailable;
}
private static JsonObject inferredBoundaryType(
String role,
Integer ordinal,
String name,
DataType dataType,
String storage,
boolean typeLocked,
String signatureSource
) {
JsonObject result = new JsonObject();
result.addProperty("role", role);
if (ordinal == null) {
result.add("ordinal", JsonNull.INSTANCE);
}
else {
result.addProperty("ordinal", ordinal);
}
if (name == null || name.isBlank()) {
result.add("name", JsonNull.INSTANCE);
}
else {
result.addProperty("name", name);
}
result.addProperty("data_type", dataType.getDisplayName());
int dataTypeLength = dataType.getLength();
if (dataTypeLength < 0) {
result.add("size_bytes", JsonNull.INSTANCE);
}
else {
result.addProperty("size_bytes", dataTypeLength);
}
if (storage.isBlank()) {
result.add("storage", JsonNull.INSTANCE);
}
else {
result.addProperty("storage", storage);
}
result.addProperty(
"confidence",
inferenceConfidence(signatureSource, typeLocked)
);
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"signature-source",
"ghidra-function-manager",
"Function signature source is " + signatureSource + "."
)
);
evidence.add(
inferenceEvidence(
"decompiler-type",
"ghidra-high-function",
"HighFunction recovered data type " + dataType.getDisplayName() + "."
)
);
if (typeLocked) {
evidence.add(
inferenceEvidence(
"type-lock",
"ghidra-high-symbol",
"The decompiler marks this parameter type as locked."
)
);
}
result.add("evidence", evidence);
JsonArray artifacts = new JsonArray();
artifacts.add("recovered-signature");
if (!storage.isBlank()) {
artifacts.add("register-or-stack-storage");
}
if (name != null && (name.startsWith("param_") || name.startsWith("unaff_"))) {
artifacts.add("compiler-generated-variable");
}
result.add("decompiler_artifacts", artifacts);
return result;
}
private JsonObject inferredJumpTable(
JumpTable jumpTable,
List<ClangNode> caseTokens
) throws Exception {
JsonObject result = new JsonObject();
result.addProperty(
"dispatch_address",
canonicalAddress(jumpTable.getSwitchAddress())
);
JumpTable.LoadTable[] loadTables = jumpTable.getLoadTables();
RecoveredAArch64RelativeTable recoveredAArch64 =
loadTables.length == 0
? recoverAArch64RelativeTable(jumpTable)
: null;
List<Address> referencedDataAddresses = loadTables.length == 0
? dispatchPathDataReferences(jumpTable)
: List.of();
JsonArray dataSources = new JsonArray();
for (int index = 0; index < loadTables.length; index += 1) {
monitor.checkCancelled();
JumpTable.LoadTable loadTable = loadTables[index];
JsonObject source = new JsonObject();
source.addProperty("address", canonicalAddress(loadTable.getAddress()));
source.addProperty("provenance", "ghidra-decompiler-load-table");
int entrySize = loadTable.getSize();
int entryCount = loadTable.getNum();
if (entrySize > 0) source.addProperty("entry_size_bytes", entrySize);
else source.add("entry_size_bytes", JsonNull.INSTANCE);
if (entryCount > 0) source.addProperty("entry_count", entryCount);
else source.add("entry_count", JsonNull.INSTANCE);
source.addProperty(
"confidence",
entrySize > 0 && entryCount > 0 ? "high" : "medium"
);
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-high-function",
"Decompiler load table starts at " +
canonicalAddress(loadTable.getAddress()) +
"."
)
);
source.add("evidence", evidence);
dataSources.add(source);
}
if (recoveredAArch64 != null) {
JsonObject source = new JsonObject();
source.addProperty(
"address",
canonicalAddress(recoveredAArch64.tableAddress)
);
source.addProperty("provenance", recoveredAArch64.entrySize == 1 ? "ghidra-aarch64-byte-table" : "ghidra-aarch64-halfword-table");
source.addProperty("entry_size_bytes", recoveredAArch64.entrySize);
source.addProperty("entry_count", recoveredAArch64.mappings.size());
source.addProperty("confidence", "high");
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-instruction-and-memory",
"Decoded " + recoveredAArch64.mappings.size() +
" entries of size " + recoveredAArch64.entrySize + " bytes from " + canonicalAddress(recoveredAArch64.tableAddress) +
"; the AArch64 LDRB-or-LDRH/ADR/ADD/BR chain uses a selector guarded by an unsigned upper-bound branch."
)
);
source.add("evidence", evidence);
dataSources.add(source);
}
for (int index = 0; index < referencedDataAddresses.size(); index += 1) {
Address address = referencedDataAddresses.get(index);
if (
recoveredAArch64 != null &&
recoveredAArch64.tableAddress.equals(address)
) {
continue;
}
JsonObject source = new JsonObject();
source.addProperty("address", canonicalAddress(address));
source.addProperty(
"provenance",
"ghidra-dispatch-block-data-reference"
);
source.add("entry_size_bytes", JsonNull.INSTANCE);
source.add("entry_count", JsonNull.INSTANCE);
source.addProperty("confidence", "medium");
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-reference-manager",
"The dispatch block or an immediate predecessor contains a typed data reference to " +
canonicalAddress(address) +
"."
)
);
source.add("evidence", evidence);
dataSources.add(source);
}
result.add("data_sources", dataSources);
Address[] targets = jumpTable.getCases();
TypedJumpTableCases typedCases = typedJumpTableCases(jumpTable, caseTokens);
JsonArray mappings = new JsonArray();
JsonArray defaultTargets = new JsonArray();
for (Address target : typedCases.defaults) {
defaultTargets.add(typedJumpTableTarget(target, jumpTable, "Typed default label"));
}
Set<Address> coveredTargets = new HashSet<>(typedCases.cases.values());
coveredTargets.addAll(typedCases.defaults);
if (recoveredAArch64 != null) {
for (int index = 0; index < recoveredAArch64.mappings.size(); index += 1) {
monitor.checkCancelled();
RecoveredAArch64Mapping recovered = recoveredAArch64.mappings.get(index);
if (
typedCases.unknownTargets.contains(recovered.targetAddress) ||
typedCases.ambiguousValues.contains((long) recovered.caseValue)
) continue;
Address typedTarget = typedCases.cases.get((long) recovered.caseValue);
if (typedTarget != null && !typedTarget.equals(recovered.targetAddress)) {
typedCases.cases.remove((long) recovered.caseValue);
typedCases.ambiguousValues.add((long) recovered.caseValue);
typedCases.unknownTargets.add(typedTarget);
typedCases.unknownTargets.add(recovered.targetAddress);
typedCases.limitations.add(
"Typed case evidence and the verified relative table disagree on a destination; both destinations remain unknown for that case."
);
continue;
}
JsonObject mapping = new JsonObject();
mapping.addProperty("case_value", recovered.caseValue);
String targetAddress = canonicalAddress(recovered.targetAddress);
mapping.addProperty("target_address", targetAddress);
mapping.addProperty("confidence", "high");
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-instruction-and-memory",
"Case value " + recovered.caseValue + (recoveredAArch64.entrySize == 1 ? " indexes byte " : " indexes halfword at byte offset ") +
(recovered.caseValue * recoveredAArch64.entrySize) + " at " + canonicalAddress(recoveredAArch64.tableAddress) +
"; the loaded unsigned entry selects a four-byte instruction offset from " +
canonicalAddress(recoveredAArch64.branchBase) + ", reaching " + targetAddress + "."
)
);
mapping.add("evidence", evidence);
if (!typedCases.cases.containsKey((long) recovered.caseValue)) {
mappings.add(mapping);
}
coveredTargets.add(recovered.targetAddress);
}
}
for (Map.Entry<Long, Address> entry : typedCases.cases.entrySet()) {
JsonObject mapping = typedJumpTableTarget(
entry.getValue(),
jumpTable,
"Typed case value " + entry.getKey()
);
mapping.addProperty("case_value", entry.getKey());
mappings.add(mapping);
}
Set<Address> emittedUnknownTargets = new HashSet<>();
for (int index = 0; index < targets.length; index += 1) {
monitor.checkCancelled();
boolean unresolvedToken = typedCases.unknownTargets.contains(targets[index]);
if (coveredTargets.contains(targets[index]) && !unresolvedToken) continue;
if (!emittedUnknownTargets.add(targets[index])) continue;
JsonObject mapping = new JsonObject();
mapping.add("case_value", JsonNull.INSTANCE);
String targetAddress = canonicalAddress(targets[index]);
mapping.addProperty("target_address", targetAddress);
// Ghidra's recovered case-to-target relation does not identify
// which backing load table produced an individual case. Keep the
// table inventory and recovered mappings separate instead of
// attaching every table address to every target.
mapping.addProperty("confidence", "medium");
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-high-function",
"Ghidra recovered target " +
targetAddress +
" at dispatch " +
canonicalAddress(jumpTable.getSwitchAddress()) +
", but no unambiguous, exactly representable case label or default relationship was available."
)
);
mapping.add("evidence", evidence);
mappings.add(mapping);
}
result.add("mappings", mappings);
result.add("default_targets", defaultTargets);
JsonArray limitations = new JsonArray();
for (String limitation : typedCases.limitations) limitations.add(limitation);
if (defaultTargets.isEmpty()) {
limitations.add(
"Ghidra exposed no typed default label with an unambiguous relationship to a recovered destination of this dispatch; default or out-of-range handling remains unknown."
);
}
if (loadTables.length == 0 && referencedDataAddresses.isEmpty()) {
limitations.add(
"Ghidra recovered targets but exposed no backing load-table or dispatch-block data address."
);
}
if (
recoveredAArch64 == null &&
loadTables.length == 0 &&
!referencedDataAddresses.isEmpty()
) {
limitations.add(
"Load-table metadata was absent; table-level candidate ownership is inferred from typed data references in the dispatch block and its immediate control-flow predecessors, but candidates are not assigned to individual mappings."
);
}
if (!emittedUnknownTargets.isEmpty()) {
limitations.add(
"Some recovered destinations lack an unambiguous, exactly representable case/default relationship; their case values remain unknown. Unequal label and destination arrays are never paired by index."
);
}
if (recoveredAArch64 == null && loadTables.length == 0 && currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:")) {
limitations.add("REA did not verify a supported byte/halfword relative dispatch chain with exact unsigned bounds and register definitions; unsupported encodings and ambiguous bounds remain unknown.");
}
if (targets.length == 0) {
limitations.add(
"Ghidra exposed a jump-table dispatch without recovered case targets."
);
}
result.add("limitations", limitations);
return result;
}
private static final class TypedJumpTableCases {
private final Map<Long, Address> cases = new TreeMap<>();
private final Set<Address> defaults = new java.util.TreeSet<>();
private final Set<Address> unknownTargets = new HashSet<>();
private final Set<Long> ambiguousValues = new HashSet<>();
private final Set<String> limitations = new java.util.LinkedHashSet<>();
}
private TypedJumpTableCases typedJumpTableCases(
JumpTable jumpTable,
List<ClangNode> tokens
) throws Exception {
TypedJumpTableCases result = new TypedJumpTableCases();
Set<Address> targets = new HashSet<>();
for (Address target : jumpTable.getCases()) targets.add(target);
for (ClangNode node : tokens) {
monitor.checkCancelled();
if (!(node instanceof ClangCaseToken token)) continue;
PcodeOp operation = token.getPcodeOp();
if (operation == null || operation.getParent() == null) continue;
PcodeBlockBasic block = operation.getParent();
Address target = block.getStart();
if (!targets.contains(target)) continue;
PcodeOp dispatch = null;
boolean ambiguousDispatch = false;
boolean includesDispatch = false;
for (int index = 0; index < block.getInSize(); index += 1) {
PcodeBlock predecessor = block.getIn(index);
if (!(predecessor instanceof PcodeBlockBasic basic)) continue;
PcodeOp last = basic.getLastOp();
if (last == null || last.getOpcode() != PcodeOp.BRANCHIND) continue;
if (last.getSeqnum().getTarget().equals(jumpTable.getSwitchAddress())) {
includesDispatch = true;
}
if (dispatch != null && dispatch != last) ambiguousDispatch = true;
dispatch = last;
}
if (!includesDispatch) continue;
if (ambiguousDispatch) {
result.unknownTargets.add(target);
result.limitations.add(
"A typed case/default block has multiple indirect dispatch predecessors; REA cannot attribute its label to one switch."
);
continue;
}
// Default is a typed label role, not an OFF sentinel: Ghidra can
// encode a default token with an ordinary in-range case value.
if ("default".equals(token.getText())) {
result.defaults.add(target);
continue;
}
Long value = exactTypedCaseValue(token);
if (value == null) {
result.unknownTargets.add(target);
result.limitations.add(
"Typed case label " + token.getText() +
" has encoded unsigned magnitude " + Long.toUnsignedString(token.getValue()) +
" at recovered target " + canonicalAddress(target) +
" for dispatch " + canonicalAddress(jumpTable.getSwitchAddress()) +
"; the label is nonliteral, inconsistent with its encoded magnitude, or outside the exact JSON integer range, so its numeric case value remains unknown."
);
continue;
}
if (result.ambiguousValues.contains(value)) {
result.unknownTargets.add(target);
continue;
}
Address previous = result.cases.putIfAbsent(value, target);
if (previous != null && !previous.equals(target)) {
result.cases.remove(value);
result.ambiguousValues.add(value);
result.unknownTargets.add(previous);
result.unknownTargets.add(target);
result.limitations.add(
"Typed tokens assign one case value to different recovered destinations; that case remains unknown."
);
}
}
return result;
}
private static Long exactTypedCaseValue(ClangCaseToken token) {
String text = token.getText();
if (text == null || !text.matches("-?(?:0[xX][0-9a-fA-F]+|[0-9]+)")) return null;
boolean negative = text.startsWith("-");
String literal = negative ? text.substring(1) : text;
BigInteger magnitude = literal.startsWith("0x") || literal.startsWith("0X")
? new BigInteger(literal.substring(2), 16)
: new BigInteger(literal, 10);
// Negative numeric tokens carry an unsigned magnitude in OFF, so
// getValue()/getScalar() alone would incorrectly emit positive cases.
if (!magnitude.equals(new BigInteger(Long.toUnsignedString(token.getValue())))) return null;
BigInteger value = negative ? magnitude.negate() : magnitude;
if (value.abs().compareTo(BigInteger.valueOf(9007199254740991L)) > 0) return null;
return value.longValueExact();
}
private static JsonObject typedJumpTableTarget(
Address target,
JumpTable table,
String label
) {
JsonObject result = new JsonObject();
String address = canonicalAddress(target);
result.addProperty("target_address", address);
result.addProperty("confidence", "high");
JsonArray evidence = new JsonArray();
evidence.add(inferenceEvidence(
"jump-table",
"ghidra-clang-case-token",
label + " belongs to the p-code block starting at recovered target " +
address + "; its unique indirect dispatch predecessor is " +
canonicalAddress(table.getSwitchAddress()) + "."
));
result.add("evidence", evidence);
return result;
}
private static final class RecoveredAArch64Mapping {
private final int caseValue;
private final Address targetAddress;
private RecoveredAArch64Mapping(int caseValue, Address targetAddress) {
this.caseValue = caseValue;
this.targetAddress = targetAddress;
}
}
private static final class RecoveredAArch64RelativeTable {
private final Address tableAddress;
private final Address branchBase;
private final int entrySize;
private final List<RecoveredAArch64Mapping> mappings;
private RecoveredAArch64RelativeTable(
Address tableAddress,
Address branchBase,
int entrySize,
List<RecoveredAArch64Mapping> mappings
) {
this.tableAddress = tableAddress;
this.branchBase = branchBase;
this.entrySize = entrySize;
this.mappings = mappings;
}
}
/**
* Recover only the directly verified AArch64 byte/halfword-indexed relative branch
* table form. Other instruction sequences keep their case labels unknown.
*/
private RecoveredAArch64RelativeTable recoverAArch64RelativeTable(
JumpTable jumpTable
) throws Exception {
if (!currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:")) {
return null;
}
Function function = containingFunction(jumpTable.getSwitchAddress());
if (function == null) return null;
List<Instruction> instructions = new ArrayList<>();
InstructionIterator iterator = currentProgram.getListing().getInstructions(
function.getBody(), true
);
while (iterator.hasNext()) {
monitor.checkCancelled();
Instruction instruction = iterator.next();
instructions.add(instruction);
if (instruction.getAddress().equals(jumpTable.getSwitchAddress())) break;
}
int branchIndex = instructions.size() - 1;
if (branchIndex < 1 ||
!instructions.get(branchIndex).getMnemonicString().equalsIgnoreCase("BR")) {
return null;
}
Address[] recoveredTargets = jumpTable.getCases();
Set<String> recoveredTargetSet = new HashSet<>();
for (Address target : recoveredTargets) recoveredTargetSet.add(canonicalAddress(target));
List<Address> dataAddresses = dispatchPathDataReferences(jumpTable);
for (int loadIndex = 0; loadIndex < branchIndex; loadIndex += 1) {
Instruction load = instructions.get(loadIndex);
int entrySize = load.getMnemonicString().equalsIgnoreCase("LDRB") ? 1 : load.getMnemonicString().equalsIgnoreCase("LDRH") ? 2 : 0;
if (entrySize == 0 || branchIndex != loadIndex + 2 || loadIndex < 3 ||
!instructions.get(loadIndex - 1).getMnemonicString().equalsIgnoreCase("ADR") ||
!instructions.get(loadIndex - 2).getMnemonicString().equalsIgnoreCase("ADD") ||
!instructions.get(loadIndex - 3).getMnemonicString().equalsIgnoreCase("ADRP")) continue;
String loadOperands = operands(load);
java.util.regex.Matcher loadMatcher = java.util.regex.Pattern
.compile(entrySize == 1 ? "(?i)^[wx]\\d+,\\s*\\[([wx]\\d+),\\s*([wx]\\d+)\\]$" : "(?i)^[wx]\\d+,\\s*\\[([wx]\\d+),\\s*([wx]\\d+),\\s*LSL\\s*#(?:0x)?1\\]$")
.matcher(loadOperands);
if (!loadMatcher.matches()) continue;
String loadedRegister = normalizedRegister(loadOperands.substring(0, loadOperands.indexOf(',')));
String tableBaseRegister = normalizedRegister(loadMatcher.group(1));
String indexRegister = normalizedRegister(loadMatcher.group(2));
Address resolvedTableAddress = null;
for (int index = loadIndex - 3; index < loadIndex - 1; index += 1) {
Instruction instruction = instructions.get(index);
String mnemonic = instruction.getMnemonicString().toUpperCase(Locale.ROOT);
String text = operands(instruction);
if (mnemonic.equals("ADRP") &&
text.matches("(?i)^[wx]\\d+,\\s*0x[0-9a-f]+$")) {
String[] parts = text.split(",");
if (normalizedRegister(parts[0]).equals(tableBaseRegister)) {
resolvedTableAddress = addressFromOperand(parts[1]);
}
}
else if (mnemonic.equals("ADD") && resolvedTableAddress != null &&
text.matches("(?i)^[wx]\\d+,\\s*[wx]\\d+,\\s*#?(?:0x[0-9a-f]+|[0-9]+)$")) {
String[] parts = text.split(",");
if (normalizedRegister(parts[0]).equals(tableBaseRegister) &&
normalizedRegister(parts[1]).equals(tableBaseRegister)) {
resolvedTableAddress = resolvedTableAddress.add(parseInteger(parts[2].trim().replaceFirst("^#", "")));
}
}
}
if (resolvedTableAddress == null || !currentProgram.getMemory().contains(resolvedTableAddress) ||
(!dataAddresses.isEmpty() && !dataAddresses.contains(resolvedTableAddress))) continue;
Address tableAddress = resolvedTableAddress;
Address branchBase = null;
boolean addressChain = false;
for (int index = loadIndex - 1; index < branchIndex; index += 1) {
Instruction instruction = instructions.get(index);
String mnemonic = instruction.getMnemonicString().toUpperCase(Locale.ROOT);
String text = operands(instruction);
if (index < loadIndex && mnemonic.equals("ADR") && text.matches("(?i)^[wx]\\d+,\\s*0x[0-9a-f]+$")) {
int comma = text.indexOf(',');
String destination = normalizedRegister(text.substring(0, comma));
if (!destination.equals(normalizedRegister(operands(instructions.get(branchIndex))))) continue;
branchBase = addressFromOperand(text.substring(comma + 1));
}
if (index > loadIndex && mnemonic.equals("ADD") && branchBase != null &&
text.matches("(?i)^[wx]\\d+,\\s*[wx]\\d+,\\s*[wx]\\d+,\\s*LSL\\s*#(?:0x)?2$")) {
String[] parts = text.split(",");
String destination = normalizedRegister(parts[0]);
String base = normalizedRegister(parts[1]);
String offset = normalizedRegister(parts[2]);
if (destination.equals(base) &&
base.equals(normalizedRegister(operands(instructions.get(branchIndex)))) &&
offset.equals(loadedRegister)) {
addressChain = true;
}
}
}
if (!addressChain || branchBase == null) continue;
int entryCount = -1;
Address defaultTarget = null;
for (int compareIndex = 0; compareIndex < loadIndex; compareIndex += 1) {
Instruction compare = instructions.get(compareIndex);
if (!compare.getMnemonicString().equalsIgnoreCase("CMP")) continue;
java.util.regex.Matcher compareMatcher = java.util.regex.Pattern
.compile("(?i)^([wx]\\d+),\\s*#?(0x[0-9a-f]+|[0-9]+)$")
.matcher(operands(compare));
if (!compareMatcher.matches()) continue;
String comparedRegister = normalizedRegister(compareMatcher.group(1));
long upperBound = parseInteger(compareMatcher.group(2));
if (upperBound < 0 || upperBound >= 4096) continue;
for (int guardIndex = compareIndex + 1; guardIndex < loadIndex; guardIndex += 1) {
Instruction guard = instructions.get(guardIndex);
String mnemonic = guard.getMnemonicString().toUpperCase(Locale.ROOT);
if (!mnemonic.equals("B.HI")) continue;
Address[] flows = guard.getFlows();
if (flows.length != 1 || !recoveredTargetSet.contains(canonicalAddress(flows[0]))) continue;
boolean supportedSequence = true;
for (int sequenceIndex = compareIndex + 1; sequenceIndex < loadIndex; sequenceIndex += 1) {
String sequenceMnemonic = instructions.get(sequenceIndex)
.getMnemonicString().toUpperCase(Locale.ROOT);
if (!Set.of("B.HI", "MOV", "ADRP", "ADD", "ADR").contains(sequenceMnemonic)) {
supportedSequence = false;
break;
}
}
Set<String> selectorAliases = new HashSet<>();
selectorAliases.add(comparedRegister);
Set<String> boundedIndexes = new HashSet<>();
if (compareMatcher.group(1).toLowerCase(Locale.ROOT).startsWith("x")) boundedIndexes.add(comparedRegister);
for (int sequenceIndex = compareIndex + 1; sequenceIndex < loadIndex; sequenceIndex++) {
Instruction instruction = instructions.get(sequenceIndex);
String[] parts = operands(instruction).split(",");
if (parts.length == 0) continue;
if (instruction.getMnemonicString().equalsIgnoreCase("MOV")) {
String destination = normalizedRegister(parts[0]);
String source = parts.length == 2 ? normalizedRegister(parts[1]) : "";
boolean aliasesSelector = selectorAliases.contains(source);
boolean fullWidthBounded = boundedIndexes.contains(source) ||
(parts[0].trim().toLowerCase(Locale.ROOT).startsWith("w") && aliasesSelector);
if (aliasesSelector) selectorAliases.add(destination);
else selectorAliases.remove(destination);
if (fullWidthBounded) boundedIndexes.add(destination);
else boundedIndexes.remove(destination);
} else if (Set.of("ADRP", "ADD", "ADR").contains(instruction.getMnemonicString().toUpperCase(Locale.ROOT))) {
selectorAliases.remove(normalizedRegister(parts[0]));
boundedIndexes.remove(normalizedRegister(parts[0]));
}
}
boolean selectorFeedsTable = boundedIndexes.contains(indexRegister);
if (compareIndex < loadIndex - 3 && selectorFeedsTable && supportedSequence) {
entryCount = (int) upperBound + 1;
defaultTarget = flows[0];
break;
}
}
if (entryCount > 0) break;
}
if (entryCount <= 0 || entryCount > 4096 || defaultTarget == null) continue;
List<RecoveredAArch64Mapping> mappings = new ArrayList<>();
Set<String> mappedTargets = new HashSet<>();
boolean valid = true;
for (int caseValue = 0; caseValue < entryCount; caseValue += 1) {
Address entry = tableAddress.add((long) caseValue * entrySize);
int encodedOffset = entrySize == 1 ? currentProgram.getMemory().getByte(entry) & 0xff : currentProgram.getMemory().getShort(entry) & 0xffff;
Address target = branchBase.add((long) encodedOffset * 4L);
String canonicalTarget = canonicalAddress(target);
if (!recoveredTargetSet.contains(canonicalTarget)) {
valid = false;
break;
}
mappedTargets.add(canonicalTarget);
mappings.add(new RecoveredAArch64Mapping(caseValue, target));
}
if (!valid || mappedTargets.contains(canonicalAddress(defaultTarget))) continue;
Set<String> accountedTargets = new HashSet<>(mappedTargets);
accountedTargets.add(canonicalAddress(defaultTarget));
if (!accountedTargets.equals(recoveredTargetSet)) continue;
return new RecoveredAArch64RelativeTable(tableAddress, branchBase, entrySize, List.copyOf(mappings));
}
return null;
}
private static String operands(Instruction instruction) {
List<String> values = new ArrayList<>();
for (int index = 0; index < instruction.getNumOperands(); index += 1) {
values.add(instruction.getDefaultOperandRepresentation(index));
}
return String.join(", ", values);
}
private static String normalizedRegister(String value) {
String register = value.trim().toLowerCase(Locale.ROOT);
return register.matches("[wx]\\d+") ? "r" + register.substring(1) : register;
}
private static long parseInteger(String value) {
String number = value.toLowerCase(Locale.ROOT);
return number.startsWith("0x")
? Long.parseUnsignedLong(number.substring(2), 16)
: Long.parseLong(number);
}
private Address addressFromOperand(String value) {
String hex = value.trim().replaceFirst("(?i)^0x", "");
return currentProgram.getAddressFactory().getDefaultAddressSpace()
.getAddress(Long.parseUnsignedLong(hex, 16));
}
private List<Address> dispatchPathDataReferences(JumpTable jumpTable)
throws Exception {
BasicBlockModel model = new BasicBlockModel(currentProgram, false);
CodeBlock dispatchBlock = model.getFirstCodeBlockContaining(
jumpTable.getSwitchAddress(),
monitor
);
if (dispatchBlock == null) {
return List.of();
}
List<CodeBlock> blocks = new ArrayList<>();
blocks.add(dispatchBlock);
CodeBlockReferenceIterator sources = dispatchBlock.getSources(monitor);
while (sources.hasNext()) {
monitor.checkCancelled();
CodeBlock sourceBlock = sources.next().getSourceBlock();
if (sourceBlock != null) {
blocks.add(sourceBlock);
}
}
Set<Address> addresses = new HashSet<>();
for (CodeBlock block : blocks) {
InstructionIterator instructions = currentProgram
.getListing()
.getInstructions(block, true);
while (instructions.hasNext()) {
monitor.checkCancelled();
Instruction instruction = instructions.next();
for (
Reference reference : currentProgram
.getReferenceManager()
.getReferencesFrom(instruction.getAddress())
) {
if (
reference.getReferenceType().isData() &&
currentProgram
.getMemory()
.getLoadedAndInitializedAddressSet()
.contains(reference.getToAddress())
) {
addresses.add(reference.getToAddress());
}
}
}
}
List<Address> ordered = new ArrayList<>(addresses);
ordered.sort(Address::compareTo);
return List.copyOf(ordered);
}
private static JsonObject inferenceEvidence(
String kind,
String source,
String detail
) {
JsonObject evidence = new JsonObject();
evidence.addProperty("kind", kind);
evidence.addProperty("source", source);
evidence.addProperty("detail", detail);
return evidence;
}
private static String inferenceConfidence(
String signatureSource,
boolean typeLocked
) {
if (
typeLocked ||
signatureSource.equals("user_defined") ||
signatureSource.equals("imported")
) {
return "high";
}
return signatureSource.equals("analysis") ? "medium" : "low";
}
private DecompileResults decompile(Function function) {
if (function.isExternal() || function.getBody().isEmpty()) {
return null;
}
DecompileResults results = decompiler.decompileFunction(
function,
0,
monitor
);
if (results.isCancelled()) {
throw new RequestFailure("decompile_cancelled", "Ghidra decompilation was cancelled");
}
if (!results.decompileCompleted()) {
throw new RequestFailure(
"decompile_failed",
"Ghidra decompilation failed: " + diagnosticMessage(results.getErrorMessage())
);
}
return results;
}
private static String decompiledText(DecompileResults results) {
if (results == null) {
return null;
}
DecompiledFunction value = results.getDecompiledFunction();
return value == null ? null : value.getC();
}
private InstructionScan scanInstructions(Function function, int maximum) throws Exception {
InstructionIterator iterator = currentProgram.getListing().getInstructions(
function.getBody(),
true
);
List<Instruction> instructions = new ArrayList<>();
while (iterator.hasNext() && instructions.size() < maximum) {
monitor.checkCancelled();
instructions.add(iterator.next());
}
return new InstructionScan(List.copyOf(instructions), iterator.hasNext());
}
private static String renderAssembly(List<Instruction> instructions) {
return String.join("\n", renderAssemblyLines(instructions));
}
private static List<String> renderAssemblyLines(List<Instruction> instructions) {
List<String> result = new ArrayList<>();
for (Instruction instruction : instructions) {
StringBuilder line = new StringBuilder();
line.append(canonicalAddress(instruction.getAddress()));
line.append(": ");
line.append(instruction.getMnemonicString());
for (int index = 0; index < instruction.getNumOperands(); index += 1) {
line.append(index == 0 ? " " : ", ");
line.append(instruction.getDefaultOperandRepresentation(index));
}
result.add(line.toString());
}
return result;
}
private List<Reference> collectReferences(
List<Instruction> instructions,
String direction) throws Exception {
TreeMap<String, Reference> observed = new TreeMap<>();
for (Instruction instruction : instructions) {
monitor.checkCancelled();
if (direction.equals("outgoing")) {
for (Reference reference : currentProgram.getReferenceManager()
.getReferencesFrom(instruction.getAddress())) {
addReference(observed, reference);
}
}
else {
ReferenceIterator iterator = currentProgram.getReferenceManager()
.getReferencesTo(instruction.getAddress());
while (iterator.hasNext()) {
addReference(observed, iterator.next());
}
}
}
List<Reference> result = new ArrayList<>(observed.values());
result.sort(REFERENCE_ORDER);
return result;
}
private static void addReference(TreeMap<String, Reference> observed, Reference reference) {
if (reference.isEntryPointReference()) {
return;
}
String key = canonicalAddress(reference.getFromAddress()) + "\u0000" +
canonicalAddress(reference.getToAddress()) + "\u0000" +
reference.getReferenceType().getName() + "\u0000" +
reference.getOperandIndex() + "\u0000" +
reference.isPrimary();
observed.putIfAbsent(key, reference);
}
private JsonArray referenceEdges(List<Reference> references) {
JsonArray result = new JsonArray();
for (Reference reference : references) {
result.add(referenceEdge(reference));
}
return result;
}
private JsonObject referenceEdge(Reference reference) {
JsonObject result = new JsonObject();
result.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
result.addProperty("target_address", canonicalAddress(reference.getToAddress()));
Function source = containingFunction(reference.getFromAddress());
Function target = containingFunction(reference.getToAddress());
result.add(
"source_procedure",
source == null ? JsonNull.INSTANCE : procedureIdentity(source)
);
result.add(
"target_procedure",
target == null ? JsonNull.INSTANCE : procedureIdentity(target)
);
result.add("kind", referenceKind(reference));
return result;
}
private static JsonObject referenceKind(Reference reference) {
RefType type = reference.getReferenceType();
JsonObject result = new JsonObject();
result.addProperty("available", true);
result.addProperty("provenance", "ghidra-reference-manager");
result.addProperty("type", type.getName());
result.addProperty("flow", type.isFlow());
result.addProperty("call", type.isCall());
result.addProperty("jump", type.isJump());
result.addProperty("data", type.isData());
result.addProperty("read", type.isRead());
result.addProperty("write", type.isWrite());
result.addProperty("indirect", type.isIndirect());
result.addProperty("computed", type.isComputed());
result.addProperty("conditional", type.isConditional());
result.addProperty("terminal", type.isTerminal());
result.addProperty("primary", reference.isPrimary());
result.addProperty("operand_index", reference.getOperandIndex());
result.addProperty("external", reference.isExternalReference());
return result;
}
private Function containingFunction(Address address) {
Function result = currentProgram.getFunctionManager().getFunctionAt(address);
if (result == null && address.isMemoryAddress()) {
result = currentProgram.getFunctionManager().getFunctionContaining(address);
}
return result;
}
private static String procedureName(Function function) {
Symbol symbol = function.getSymbol();
return symbol == null ? function.getName() : symbol.getName(true);
}
private static JsonObject functionClassification(Function function) {
JsonObject result = new JsonObject();
result.addProperty("external", function.isExternal());
result.addProperty("thunk", function.isThunk());
Function target = function.isThunk() ? function.getThunkedFunction(false) : null;
if (target == null) {
result.add("thunk_target", JsonNull.INSTANCE);
}
else {
result.addProperty("thunk_target", canonicalAddress(target.getEntryPoint()));
}
result.addProperty("provenance", "ghidra-function-manager");
return result;
}
private static JsonArray functionLocals(Function function) {
Variable[] variables = function.getLocalVariables();
List<Variable> ordered = new ArrayList<>(List.of(variables));
ordered.sort(
Comparator.comparing(Variable::getName)
.thenComparing(variable -> variable.getVariableStorage().toString())
);
JsonArray result = new JsonArray();
for (Variable variable : ordered) {
JsonObject item = new JsonObject();
item.addProperty(
"description",
variable.getDataType().getDisplayName() + " " + variable.getName() +
" @ " + variable.getVariableStorage()
);
item.addProperty("provenance", "ghidra-function-database");
result.add(item);
}
return result;
}
private JsonArray procedureIdentities(Set<Function> functions) {
List<Function> ordered = new ArrayList<>(functions);
ordered.sort(Comparator.comparing(Function::getEntryPoint));
JsonArray result = new JsonArray();
for (Function function : ordered) {
result.add(procedureIdentity(function));
}
return result;
}
private List<CodeBlock> basicBlocks(Function function) throws Exception {
BasicBlockModel model = new BasicBlockModel(currentProgram, false);
CodeBlockIterator iterator = model.getCodeBlocksContaining(function.getBody(), monitor);
List<CodeBlock> result = new ArrayList<>();
while (iterator.hasNext()) {
monitor.checkCancelled();
result.add(iterator.next());
}
result.sort(Comparator.comparing(CodeBlock::getFirstStartAddress));
return result;
}
private JsonArray basicBlockValues(Function function) throws Exception {
JsonArray result = new JsonArray();
for (CodeBlock block : basicBlocks(function)) {
JsonObject item = new JsonObject();
item.addProperty("start", canonicalAddress(block.getFirstStartAddress()));
item.addProperty("end", exclusiveAddress(block.getMaxAddress()));
Set<Address> successors = new HashSet<>();
CodeBlockReferenceIterator destinations = block.getDestinations(monitor);
while (destinations.hasNext()) {
CodeBlockReference destination = destinations.next();
CodeBlock destinationBlock = destination.getDestinationBlock();
if (destinationBlock == null) {
continue;
}
Address address = destinationBlock.getFirstStartAddress();
if (
!destination.getFlowType().isCall() &&
function.getBody().contains(address)
) {
successors.add(address);
}
}
List<Address> ordered = new ArrayList<>(successors);
ordered.sort(Address::compareTo);
JsonArray values = new JsonArray();
for (Address address : ordered) {
values.add(canonicalAddress(address));
}
item.add("successors", values);
result.add(item);
}
return result;
}
private static JsonArray comments(List<Instruction> instructions) {
TreeMap<String, JsonObject> observed = new TreeMap<>();
for (Instruction instruction : instructions) {
for (CommentType type : CommentType.values()) {
String text = instruction.getComment(type);
if (text == null || text.isEmpty()) {
continue;
}
String kind = type == CommentType.EOL ? "inline" : "comment";
JsonObject item = new JsonObject();
item.addProperty("address", canonicalAddress(instruction.getAddress()));
item.addProperty("kind", kind);
item.addProperty("text", text);
String key = canonicalAddress(instruction.getAddress()) + "\u0000" +
kind + "\u0000" + type.name() + "\u0000" + text;
observed.put(key, item);
}
}
JsonArray result = new JsonArray();
observed.values().forEach(result::add);
return result;
}
private JsonArray referencedStrings(List<Reference> references) {
TreeMap<String, JsonObject> observed = new TreeMap<>();
for (Reference reference : references) {
Data data = currentProgram.getListing().getDefinedDataContaining(
reference.getToAddress()
);
if (data == null || !data.hasStringValue()) {
continue;
}
String value = StringDataInstance.getStringDataInstance(data).getStringValue();
if (value == null) {
continue;
}
JsonObject item = new JsonObject();
item.addProperty("address", canonicalAddress(data.getAddress()));
item.addProperty("value", value);
item.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
String key = canonicalAddress(data.getAddress()) + "\u0000" + value + "\u0000" +
canonicalAddress(reference.getFromAddress());
observed.put(key, item);
}
JsonArray result = new JsonArray();
observed.values().forEach(result::add);
return result;
}
private JsonArray referencedNames(List<Reference> references) {
TreeMap<String, JsonObject> observed = new TreeMap<>();
for (Reference reference : references) {
Symbol symbol = currentProgram.getSymbolTable().getPrimarySymbol(
reference.getToAddress()
);
if (symbol == null) {
continue;
}
JsonObject item = new JsonObject();
item.addProperty("address", canonicalAddress(reference.getToAddress()));
item.addProperty("value", symbol.getName(true));
item.addProperty("source_address", canonicalAddress(reference.getFromAddress()));
String key = canonicalAddress(reference.getToAddress()) + "\u0000" +
symbol.getName(true) + "\u0000" + canonicalAddress(reference.getFromAddress());
observed.put(key, item);
}
JsonArray result = new JsonArray();
observed.values().forEach(result::add);
return result;
}
private static String exclusiveAddress(Address inclusive) {
Address next = inclusive.next();
if (next != null && next.getAddressSpace().equals(inclusive.getAddressSpace())) {
return canonicalAddress(next);
}
BigInteger offset = new BigInteger(Long.toUnsignedString(inclusive.getOffset()));
return canonicalOffset(inclusive.getAddressSpace(), offset.add(BigInteger.ONE));
}
private static String diagnosticMessage(String value) {
if (value == null || value.isBlank()) {
return "native decompiler returned no detail";
}
return value.replaceAll("[\\r\\n]+", " ");
}
private JsonArray inventory(
List<InventoryItem> inventory,
String factsName) throws Exception {
JsonArray result = new JsonArray();
for (InventoryItem item : inventory) {
monitor.checkCancelled();
result.add(resultItem(item, factsName));
}
return result;
}
private static JsonObject resultItem(InventoryItem item, String factsName) {
JsonObject result = new JsonObject();
result.addProperty("address", canonicalAddress(item.address));
result.addProperty("value", item.value);
if (factsName != null) {
result.add(factsName, item.facts.deepCopy());
}
return result;
}
private static JsonObject memoryRegion(MemoryBlock block, String imageBase) {
JsonObject permissions = new JsonObject();
permissions.addProperty("available", true);
permissions.addProperty("source", "ghidra-memory-block");
JsonObject result = new JsonObject();
result.addProperty("name", block.getName());
result.addProperty("start", canonicalAddress(block.getStart()));
result.addProperty("end", exclusiveEnd(block));
result.addProperty("readable", block.isRead());
result.addProperty("writable", block.isWrite());
result.addProperty("executable", block.isExecute());
result.add("permissions", permissions);
result.addProperty("provenance", "ghidra-memory-block");
result.addProperty("address_space", block.getStart().getAddressSpace().getName());
result.addProperty("image_base", imageBase);
result.addProperty("initialized", block.isInitialized());
result.addProperty("overlay", block.isOverlay());
return result;
}
private static JsonObject procedureIdentity(Function function) {
JsonObject result = new JsonObject();
result.addProperty("address", canonicalAddress(function.getEntryPoint()));
result.addProperty("name", procedureName(function));
result.add("classification", functionClassification(function));
result.add("body", functionBody(function));
return result;
}
/** Observe the entire AddressSet; range endpoints are inclusive, not a guessed span. */
private static JsonObject functionBody(Function function) {
JsonArray ranges = new JsonArray();
BigInteger total = BigInteger.ZERO;
Address first = null;
Address last = null;
AddressSpace space = null;
boolean multipleSpaces = false;
AddressRangeIterator iterator = function.getBody().getAddressRanges(true);
while (iterator.hasNext()) {
AddressRange range = iterator.next();
Address start = range.getMinAddress();
Address end = range.getMaxAddress();
if (start.getAddressSpace().getAddressableUnitSize() != 1) {
throw new IllegalStateException("Function body byte coverage requires a byte-addressed space");
}
JsonObject value = new JsonObject();
value.addProperty("start", canonicalAddress(start));
value.addProperty("end", canonicalAddress(end));
ranges.add(value);
BigInteger begin = new BigInteger(Long.toUnsignedString(start.getOffset()));
BigInteger finish = new BigInteger(Long.toUnsignedString(end.getOffset()));
total = total.add(finish.subtract(begin).add(BigInteger.ONE));
if (first == null) {
first = start;
space = start.getAddressSpace();
} else if (!space.equals(start.getAddressSpace())) {
multipleSpaces = true;
}
last = end;
}
JsonObject result = new JsonObject();
result.addProperty("available", true);
result.addProperty("provenance", "ghidra-function-body-address-set");
result.add("ranges", ranges);
result.addProperty("total_bytes", safeBodyByteCount(total));
if (multipleSpaces) {
result.add("span_bytes", JsonNull.INSTANCE);
} else {
BigInteger span = first == null ? BigInteger.ZERO :
new BigInteger(Long.toUnsignedString(last.getOffset()))
.subtract(new BigInteger(Long.toUnsignedString(first.getOffset())))
.add(BigInteger.ONE);
result.addProperty("span_bytes", safeBodyByteCount(span));
}
result.addProperty("non_contiguous", ranges.size() > 1);
result.addProperty("contains_entry", function.getBody().contains(function.getEntryPoint()));
return result;
}
private static long safeBodyByteCount(BigInteger value) {
if (value.signum() < 0 || value.compareTo(BigInteger.valueOf(9007199254740991L)) > 0) {
throw new IllegalStateException("Function body byte coverage exceeds exact JSON integer range");
}
return value.longValueExact();
}
private static String exclusiveEnd(MemoryBlock block) {
Address next = block.getEnd().next();
if (next != null && next.getAddressSpace().equals(block.getStart().getAddressSpace())) {
return canonicalAddress(next);
}
BigInteger start = new BigInteger(Long.toUnsignedString(block.getStart().getOffset()));
return canonicalOffset(
block.getStart().getAddressSpace(),
start.add(BigInteger.valueOf(block.getSize()))
);
}
private static String canonicalAddress(Address address) {
return canonicalOffset(
address.getAddressSpace(),
new BigInteger(Long.toUnsignedString(address.getOffset()))
);
}
private static String canonicalOffset(AddressSpace space, BigInteger offset) {
String value = "0x" + offset.toString(16);
if (space.equals(sessionDefaultAddressSpace)) {
return value;
}
return encodeAddressSpace(space.getName()) + ":" + value;
}
private Address requireAddress(JsonObject params, String name) {
return parseReaAddress(requireString(params, name));
}
private Address parseReaAddress(String value) {
Address address = tryParseAddress(value);
if (address == null) {
throw new RequestFailure("invalid_request", "Unknown or invalid Ghidra address: " + value);
}
return address;
}
private Address tryParseAddress(String value) {
try {
String spaceName = null;
String offset = value;
int separator = value.toLowerCase(Locale.ROOT).lastIndexOf(":0x");
if (separator >= 0) {
spaceName = decodeAddressSpace(value.substring(0, separator));
offset = value.substring(separator + 3);
}
else if (value.startsWith("0x") || value.startsWith("0X")) {
offset = value.substring(2);
}
if (!offset.matches("[0-9A-Fa-f]+")) {
return null;
}
AddressSpace space = spaceName == null
? currentProgram.getAddressFactory().getDefaultAddressSpace()
: currentProgram.getAddressFactory().getAddressSpace(spaceName);
return space == null ? null : space.getAddress(offset);
}
catch (Exception exception) {
return null;
}
}
private static String encodeAddressSpace(String value) {
StringBuilder result = new StringBuilder();
for (byte item : value.getBytes(StandardCharsets.UTF_8)) {
int unsigned = Byte.toUnsignedInt(item);
if ((unsigned >= 'A' && unsigned <= 'Z') ||
(unsigned >= 'a' && unsigned <= 'z') ||
(unsigned >= '0' && unsigned <= '9') ||
unsigned == '.' || unsigned == '_' || unsigned == '~' || unsigned == '-') {
result.append((char) unsigned);
}
else {
result.append('%');
result.append(Character.toUpperCase(Character.forDigit(unsigned >>> 4, 16)));
result.append(Character.toUpperCase(Character.forDigit(unsigned & 0xf, 16)));
}
}
return result.toString();
}
private static String decodeAddressSpace(String value) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
for (int index = 0; index < value.length();) {
char item = value.charAt(index);
if (item == '%') {
if (index + 2 >= value.length()) {
throw new RequestFailure("invalid_request", "Address space encoding is invalid");
}
int high = Character.digit(value.charAt(index + 1), 16);
int low = Character.digit(value.charAt(index + 2), 16);
if (high < 0 || low < 0) {
throw new RequestFailure("invalid_request", "Address space encoding is invalid");
}
bytes.write((high << 4) | low);
index += 3;
}
else if ((item >= 'A' && item <= 'Z') ||
(item >= 'a' && item <= 'z') ||
(item >= '0' && item <= '9') ||
item == '.' || item == '_' || item == '~' || item == '-') {
bytes.write((byte) item);
index += 1;
}
else {
throw new RequestFailure("invalid_request", "Address space encoding is invalid");
}
}
return bytes.toString(StandardCharsets.UTF_8);
}
private static String normalizedSymbolType(Symbol symbol) {
String value = symbol.getSymbolType().toString()
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "_")
.replaceAll("^_+|_+$", "");
return value.isEmpty() ? "unknown" : value;
}
private static String normalizedSource(SourceType source) {
return source.name().toLowerCase(Locale.ROOT);
}
private static ValueMatcher literalMatcher(String pattern, boolean caseSensitive) {
String needle = caseSensitive ? pattern : pattern.toLowerCase(Locale.ROOT);
return value -> {
String candidate = caseSensitive ? value : value.toLowerCase(Locale.ROOT);
return candidate.contains(needle);
};
}
private static ValueMatcher regexMatcher(String expression, boolean caseSensitive) {
Pattern pattern;
try {
pattern = Pattern.compile(
expression,
caseSensitive ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE
);
}
catch (PatternSyntaxException exception) {
throw new RequestFailure("invalid_request", "Invalid regex pattern " + expression + ": " + exception.getDescription() + " at index " + exception.getIndex());
}
return value -> pattern.matcher(value).find();
}
private static SessionDescriptor readDescriptor(Path path) throws IOException {
if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
throw new IllegalArgumentException("REA session descriptor is invalid");
}
JsonElement parsed = JsonParser.parseString(Files.readString(path, StandardCharsets.UTF_8));
Set<String> keys = new HashSet<>(DESCRIPTOR_KEYS);
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("analysis_extensions"))
keys.add("analysis_extensions");
JsonObject object = requireObject(parsed, keys);
return new SessionDescriptor(
requireString(object, "transport"),
requireString(object, "endpoint_path"),
requireString(object, "token"),
requireString(object, "run_id"),
requireSha256(object, "target_sha256"),
requireString(object, "provider_version"),
requireString(object, "profile_digest"),
object.has("analysis_extensions") ? object.getAsJsonArray("analysis_extensions") : new JsonArray()
);
}
private static Request parseRequest(String line, String expectedToken) {
JsonObject object = requireObject(JsonParser.parseString(line), REQUEST_KEYS);
int id = requireInteger(object, "id");
if (id <= 0 || !constantTimeEquals(requireString(object, "token"), expectedToken)) {
throw new IllegalArgumentException("Bridge request authentication failed");
}
JsonElement params = object.get("params");
if (params == null || !params.isJsonObject()) {
throw new IllegalArgumentException("Bridge request parameters are invalid");
}
return new Request(id, requireString(object, "method"), params.getAsJsonObject());
}
private static JsonObject requireObject(JsonElement element, Set<String> keys) {
if (!element.isJsonObject()) {
throw new IllegalArgumentException("JSON object required");
}
JsonObject object = element.getAsJsonObject();
if (!object.keySet().equals(keys)) {
throw new IllegalArgumentException("JSON object fields are invalid");
}
return object;
}
private static void requireKeys(JsonObject object, Set<String> keys) {
if (!object.keySet().equals(keys)) {
throw new RequestFailure("invalid_request", "Bridge request fields are invalid");
}
}
private void requireDocument(JsonObject params) {
String document = optionalString(params, "document");
if (document != null && !document.equals(currentProgram.getName())) {
throw new RequestFailure("not_found", "Unknown Ghidra Program identity: " + document + "; active program: " + currentProgram.getName());
}
}
private static String optionalString(JsonObject object, String name) {
JsonElement value = object.get(name);
if (value == null || value.isJsonNull()) {
return null;
}
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
throw new RequestFailure("invalid_request", name + " must be a string or null");
}
String result = value.getAsString();
if (result.isEmpty()) {
throw new RequestFailure("invalid_request", name + " cannot be empty");
}
return result;
}
private static String requireString(JsonObject object, String name) {
JsonElement value = object.get(name);
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
throw new RequestFailure("invalid_request", name + " must be a string");
}
String result = value.getAsString();
if (result.isEmpty()) {
throw new RequestFailure("invalid_request", name + " cannot be empty");
}
return result;
}
private static String requireSha256(JsonObject object, String name) {
String value = requireString(object, name);
if (!value.matches("[a-f0-9]{64}")) {
throw new IllegalArgumentException(name + " must be lowercase SHA-256");
}
return value;
}
private static int requireInteger(JsonObject object, String name) {
JsonElement value = object.get(name);
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber()) {
throw new IllegalArgumentException("JSON integer required");
}
int result = value.getAsInt();
if (value.getAsDouble() != result) {
throw new IllegalArgumentException("JSON integer required");
}
return result;
}
private static int requireBoundedInteger(
JsonObject object,
String name,
int minimum,
int maximum) {
try {
int value = requireInteger(object, name);
if (value < minimum || value > maximum) {
throw new RequestFailure(
"invalid_request",
name + " is outside its supported range"
);
}
return value;
}
catch (IllegalArgumentException exception) {
throw new RequestFailure("invalid_request", name + " must be an integer");
}
}
private static boolean requireBoolean(JsonObject object, String name) {
JsonElement value = object.get(name);
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean()) {
throw new RequestFailure("invalid_request", name + " must be a boolean");
}
return value.getAsBoolean();
}
private static boolean constantTimeEquals(String provided, String expected) {
return MessageDigest.isEqual(
provided.getBytes(StandardCharsets.UTF_8),
expected.getBytes(StandardCharsets.UTF_8)
);
}
private static void writeSuccess(BufferedWriter writer, int id, JsonElement result)
throws IOException {
JsonObject response = new JsonObject();
response.addProperty("id", id);
response.addProperty("ok", true);
response.add("result", result);
writeResponse(writer, response);
}
private static void writeFailure(
BufferedWriter writer,
int id,
String code,
String message) throws IOException {
JsonObject error = new JsonObject();
error.addProperty("code", code);
error.addProperty("message", message);
JsonObject response = new JsonObject();
response.addProperty("id", id);
response.addProperty("ok", false);
response.add("error", error);
writeResponse(writer, response);
}
private static void writeResponse(BufferedWriter writer, JsonObject response) throws IOException {
String encoded = GSON.toJson(response);
// Gson leaves UTF-16 surrogates literal. Escape them in JSON so even
// diagnostics quoting ill-formed caller text remain lossless UTF-8.
int start = 0;
for (int index = 0; index < encoded.length(); index++) {
if (!Character.isSurrogate(encoded.charAt(index))) continue;
writer.write(encoded, start, index - start);
writer.write("\\u" + Integer.toHexString(encoded.charAt(index)));
start = index + 1;
}
writer.write(encoded, start, encoded.length() - start);
writer.newLine();
writer.flush();
}
private static String safeMessage(Exception exception) {
String message = exception.getMessage();
return message == null || message.isBlank() ? "operation failed" : message;
}
private interface ValueMatcher {
boolean matches(String value);
}
private static final Comparator<InventoryItem> INVENTORY_ORDER =
Comparator.comparing(InventoryItem::address)
.thenComparing(InventoryItem::value)
.thenComparing(item -> GSON.toJson(item.facts));
private static final Comparator<Reference> REFERENCE_ORDER =
Comparator.comparing(Reference::getFromAddress)
.thenComparing(Reference::getToAddress)
.thenComparing(reference -> reference.getReferenceType().getName())
.thenComparingInt(Reference::getOperandIndex)
.thenComparing(Reference::isPrimary);
private record InventoryItem(Address address, String value, JsonObject facts) {}
private record FunctionEntry(Function function, InventoryItem item) {}
private record InstructionScan(List<Instruction> instructions, boolean truncated) {}
private record SessionDescriptor(
String transport,
String endpointPath,
String token,
String runId,
String targetSha256,
String providerVersion,
String profileDigest,
JsonArray analysisExtensions
) {}
private record Request(int id, String method, JsonObject params) {}
private static final class RequestFailure extends RuntimeException {
private final String code;
RequestFailure(String code, String message) {
super(message);
this.code = code;
}
}
}