954 lines
35 KiB
Java
954 lines
35 KiB
Java
// SPDX-License-Identifier: Apache-2.0
|
|
// Ghidra headless post-script for ghidra-cli. This file intentionally has no
|
|
// package.
|
|
|
|
import ghidra.app.decompiler.DecompInterface;
|
|
import ghidra.app.decompiler.DecompileResults;
|
|
import ghidra.app.util.headless.HeadlessScript;
|
|
import ghidra.framework.Application;
|
|
import ghidra.program.model.address.Address;
|
|
import ghidra.program.model.address.AddressFactory;
|
|
import ghidra.program.model.address.AddressSpace;
|
|
import ghidra.program.model.listing.Function;
|
|
import ghidra.program.model.listing.FunctionIterator;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.math.BigInteger;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.charset.CharacterCodingException;
|
|
import java.nio.charset.CodingErrorAction;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.AtomicMoveNotSupportedException;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.StandardCopyOption;
|
|
import java.nio.file.StandardOpenOption;
|
|
import java.util.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
/** Strict, bounded, read-only adapter used only by the ghidr harness. */
|
|
public final class GhidrAdapter extends HeadlessScript {
|
|
static final long PROTOCOL_VERSION = 1;
|
|
static final int MAX_REQUEST_BYTES = 1_048_576;
|
|
static final long MAX_RESPONSE_BYTES = 268_435_456L;
|
|
static final Set<String> DISPATCH = Set.of("doctor", "inspect", "functions", "decompile");
|
|
|
|
@Override
|
|
protected void run() throws Exception {
|
|
String[] arguments = getScriptArgs();
|
|
if (arguments.length != 2) {
|
|
throw new AdapterException(
|
|
"protocol_violation", "adapter requires request and response paths");
|
|
}
|
|
Path requestPath = Path.of(arguments[0]);
|
|
Path responsePath = Path.of(arguments[1]);
|
|
if (!requestPath.isAbsolute() || !responsePath.isAbsolute()) {
|
|
throw new AdapterException("protocol_violation", "adapter paths must be absolute");
|
|
}
|
|
Path temporaryPath = responsePath.resolveSibling(responsePath.getFileName() + ".tmp");
|
|
Request request = null;
|
|
try {
|
|
request = parseRequest(readBounded(requestPath));
|
|
if (!request.operation.equals("doctor") && analysisTimeoutOccurred()) {
|
|
throw new AdapterException("analysis_timeout", "Ghidra auto-analysis timed out");
|
|
}
|
|
Object data = dispatch(request);
|
|
writeResponse(temporaryPath, responsePath, response(request, "success", data, null));
|
|
} catch (ResponseTooLargeException error) {
|
|
Files.deleteIfExists(temporaryPath);
|
|
if (request == null) {
|
|
throw error;
|
|
}
|
|
Map<String, Object> details = object();
|
|
details.put("limit_bytes", MAX_RESPONSE_BYTES);
|
|
details.put("observed_at_least_bytes", error.observedAtLeast);
|
|
details.put("operation", request.operation);
|
|
writeResponse(temporaryPath, responsePath,
|
|
response(request, "error", null,
|
|
new AdapterError("result_too_large", "adapter result exceeds 256 MiB", details)));
|
|
} catch (AdapterException error) {
|
|
Files.deleteIfExists(temporaryPath);
|
|
if (request == null) {
|
|
throw error;
|
|
}
|
|
writeResponse(temporaryPath, responsePath,
|
|
response(request, "error", null,
|
|
new AdapterError(error.code, error.getMessage(), error.details)));
|
|
}
|
|
}
|
|
|
|
private Object dispatch(Request request) throws Exception {
|
|
return switch (request.operation) {
|
|
case "doctor" -> doctor(request.arguments);
|
|
case "inspect" -> inspect(request.arguments);
|
|
case "functions" -> functions(request.arguments);
|
|
case "decompile" -> decompile(request.arguments, request.decompileTimeoutSeconds);
|
|
default ->
|
|
throw new AdapterException("protocol_violation", "operation is not dispatched by Java");
|
|
};
|
|
}
|
|
|
|
private Object doctor(Map<String, Object> arguments) throws AdapterException {
|
|
expectKeys(arguments, Set.of("kind"), "doctor arguments");
|
|
requireKind(arguments, "doctor");
|
|
Map<String, Object> result = object();
|
|
result.put("ready", true);
|
|
result.put("ghidra_version", Application.getApplicationVersion());
|
|
result.put("java_version", Runtime.version().feature());
|
|
result.put("adapter_protocol_version", PROTOCOL_VERSION);
|
|
return result;
|
|
}
|
|
|
|
private Object inspect(Map<String, Object> arguments) throws AdapterException {
|
|
expectProgram();
|
|
expectKeys(arguments, Set.of("kind"), "inspect arguments");
|
|
requireKind(arguments, "inspect");
|
|
Map<String, Object> program = object();
|
|
program.put("image_base", address(currentProgram.getImageBase()));
|
|
program.put("minimum_address", address(currentProgram.getMinAddress()));
|
|
program.put("maximum_address", address(currentProgram.getMaxAddress()));
|
|
program.put("function_count", allFunctions().size());
|
|
return queryResult(program);
|
|
}
|
|
|
|
private Object functions(Map<String, Object> arguments) throws AdapterException {
|
|
expectProgram();
|
|
expectKeys(arguments, Set.of("kind", "page"), "functions arguments");
|
|
requireKind(arguments, "functions");
|
|
Map<String, Object> pageRequest = asObject(arguments.get("page"), "page");
|
|
expectKeys(pageRequest, Set.of("offset", "limit"), "page");
|
|
long offset = nonNegativeLong(pageRequest.get("offset"), "offset");
|
|
Object limitValue = pageRequest.get("limit");
|
|
Long limit = parseLimit(limitValue);
|
|
|
|
List<Function> all = allFunctions();
|
|
long total = all.size();
|
|
int start = (int) Math.min(offset, total);
|
|
long requestedEnd = limit == null ? total : Math.min(total, saturatingAdd(offset, limit));
|
|
int end = (int) Math.max(start, requestedEnd);
|
|
List<Object> items = new ArrayList<>();
|
|
for (Function function : all.subList(start, end)) {
|
|
items.add(functionItem(function));
|
|
}
|
|
Map<String, Object> page = object();
|
|
page.put("order", "location_then_entry_ascending");
|
|
page.put("offset", offset);
|
|
page.put("limit", limit);
|
|
page.put("returned", items.size());
|
|
page.put("total", total);
|
|
page.put("has_more", end < total);
|
|
Map<String, Object> result = object();
|
|
result.put("page", page);
|
|
result.put("items", items);
|
|
return queryResult(result);
|
|
}
|
|
|
|
private Object decompile(Map<String, Object> arguments, long timeoutSeconds) throws Exception {
|
|
expectProgram();
|
|
expectKeys(arguments, Set.of("kind", "selector"), "decompile arguments");
|
|
requireKind(arguments, "decompile");
|
|
Map<String, Object> selector = asObject(arguments.get("selector"), "selector");
|
|
expectKeys(selector, Set.of("kind", "value"), "selector");
|
|
String kind = string(selector.get("kind"), "selector kind");
|
|
Function function;
|
|
if (kind.equals("name")) {
|
|
String value = string(selector.get("value"), "selector value");
|
|
function = resolveName(value);
|
|
} else if (kind.equals("address")) {
|
|
function = resolveAddress(asObject(selector.get("value"), "selector address"));
|
|
} else {
|
|
throw new AdapterException("protocol_violation", "unknown Function Selector kind");
|
|
}
|
|
if (function.isExternal()) {
|
|
throw new AdapterException(
|
|
"function_not_decompilable", "external Function cannot be decompiled");
|
|
}
|
|
|
|
String text;
|
|
DecompInterface decompiler = new DecompInterface();
|
|
try {
|
|
if (!decompiler.openProgram(currentProgram)) {
|
|
throw new AdapterException(
|
|
"decompilation_failed", "Ghidra decompiler did not open Program");
|
|
}
|
|
int seconds = (int) Math.min(timeoutSeconds, Integer.MAX_VALUE);
|
|
DecompileResults results = decompiler.decompileFunction(function, seconds, monitor);
|
|
if (!results.decompileCompleted()) {
|
|
String message = results.getErrorMessage();
|
|
if (message != null && message.toLowerCase(java.util.Locale.ROOT).contains("timeout")) {
|
|
throw new AdapterException("decompilation_timeout", "Ghidra decompilation timed out");
|
|
}
|
|
throw new AdapterException("decompilation_failed",
|
|
message == null || message.isBlank() ? "Ghidra decompilation failed" : message);
|
|
}
|
|
text = normalizeLineEndings(results.getDecompiledFunction().getC());
|
|
} finally {
|
|
decompiler.dispose();
|
|
}
|
|
Map<String, Object> selected = object();
|
|
selected.put("name", function.getName(false));
|
|
selected.put("qualified_name", function.getName(true));
|
|
selected.put("entry", address(function.getEntryPoint()));
|
|
Map<String, Object> decompilation = object();
|
|
decompilation.put("syntax", "ghidra_c");
|
|
decompilation.put("text", text);
|
|
Map<String, Object> result = object();
|
|
result.put("requested_selector", selector);
|
|
result.put("function", selected);
|
|
result.put("decompilation", decompilation);
|
|
return queryResult(result);
|
|
}
|
|
|
|
private Function resolveName(String requested) throws AdapterException {
|
|
boolean qualified = requested.contains("::");
|
|
List<Function> matches = new ArrayList<>();
|
|
for (Function function : allFunctions()) {
|
|
String candidate = qualified ? function.getName(true) : function.getName(false);
|
|
if (candidate.equals(requested)) {
|
|
matches.add(function);
|
|
}
|
|
}
|
|
if (matches.isEmpty()) {
|
|
throw new AdapterException("function_not_found", "Function Selector did not resolve");
|
|
}
|
|
if (matches.size() != 1) {
|
|
Map<String, Object> details = object();
|
|
List<Object> candidates = new ArrayList<>();
|
|
for (Function function : matches.subList(0, Math.min(100, matches.size()))) {
|
|
candidates.add(address(function.getEntryPoint()));
|
|
}
|
|
details.put("candidates", candidates);
|
|
details.put("candidate_count", matches.size());
|
|
throw new AdapterException("function_selector_ambiguous",
|
|
"Function Selector resolves to more than one Function", details);
|
|
}
|
|
return matches.get(0);
|
|
}
|
|
|
|
private Function resolveAddress(Map<String, Object> selector) throws AdapterException {
|
|
expectKeys(selector, Set.of("space", "offset"), "selector address");
|
|
String spaceName = string(selector.get("space"), "address space");
|
|
String offsetText = string(selector.get("offset"), "address offset");
|
|
AddressFactory factory = currentProgram.getAddressFactory();
|
|
AddressSpace selectedSpace = null;
|
|
List<String> spaces = new ArrayList<>();
|
|
for (AddressSpace space : factory.getAllAddressSpaces()) {
|
|
spaces.add(space.getName());
|
|
if (space.getName().equals(spaceName)) {
|
|
selectedSpace = space;
|
|
}
|
|
}
|
|
if (selectedSpace == null) {
|
|
spaces.sort(GhidrAdapter::compareUtf8);
|
|
Map<String, Object> details = object();
|
|
details.put("valid_address_spaces", spaces.subList(0, Math.min(100, spaces.size())));
|
|
throw new AdapterException(
|
|
"address_space_not_found", "address space does not exist", details);
|
|
}
|
|
int width = Math.max(2, (selectedSpace.getSize() + 3) / 4);
|
|
if (!offsetText.matches("0x[0-9a-f]+") || offsetText.length() != width + 2) {
|
|
throw new AdapterException(
|
|
"protocol_violation", "address offset is not canonical for its space");
|
|
}
|
|
BigInteger offset = new BigInteger(offsetText.substring(2), 16);
|
|
if (offset.bitLength() > 64) {
|
|
throw new AdapterException(
|
|
"protocol_violation", "address offset exceeds adapter representation");
|
|
}
|
|
Address requested;
|
|
try {
|
|
requested = selectedSpace.getAddress(offset.longValue());
|
|
} catch (RuntimeException error) {
|
|
throw new AdapterException("function_not_found", "address is outside its address space");
|
|
}
|
|
Function exact = currentProgram.getFunctionManager().getFunctionAt(requested);
|
|
if (exact != null) {
|
|
return exact;
|
|
}
|
|
Function containing = currentProgram.getFunctionManager().getFunctionContaining(requested);
|
|
if (containing != null) {
|
|
Map<String, Object> details = object();
|
|
details.put("containing_function_entry", address(containing.getEntryPoint()));
|
|
throw new AdapterException(
|
|
"function_entry_required", "address is inside a Function but is not its entry", details);
|
|
}
|
|
throw new AdapterException(
|
|
"function_not_found", "no Function exists at the requested entry Address");
|
|
}
|
|
|
|
private List<Function> allFunctions() {
|
|
LinkedHashSet<Function> unique = new LinkedHashSet<>();
|
|
FunctionIterator memory = currentProgram.getFunctionManager().getFunctions(true);
|
|
while (memory.hasNext()) {
|
|
unique.add(memory.next());
|
|
}
|
|
FunctionIterator external = currentProgram.getFunctionManager().getExternalFunctions();
|
|
while (external.hasNext()) {
|
|
unique.add(external.next());
|
|
}
|
|
List<Function> functions = new ArrayList<>(unique);
|
|
functions.sort(Comparator.comparingInt((Function function) -> function.isExternal() ? 1 : 0)
|
|
.thenComparing(function
|
|
-> function.getEntryPoint().getAddressSpace().getName(),
|
|
GhidrAdapter::compareUtf8)
|
|
.thenComparing(
|
|
function -> unsigned(function.getEntryPoint().getOffset()), BigInteger::compareTo));
|
|
return functions;
|
|
}
|
|
|
|
private Map<String, Object> functionItem(Function function) {
|
|
Map<String, Object> item = object();
|
|
item.put("name", function.getName(false));
|
|
item.put("qualified_name", function.getName(true));
|
|
item.put("entry", address(function.getEntryPoint()));
|
|
item.put("body_address_count", function.getBody().getNumAddresses());
|
|
item.put("location", function.isExternal() ? "external" : "memory");
|
|
item.put("is_external", function.isExternal());
|
|
item.put("is_thunk", function.isThunk());
|
|
Function thunk = function.getThunkedFunction(false);
|
|
item.put("thunk_target_entry", thunk == null ? null : address(thunk.getEntryPoint()));
|
|
item.put("decompilable", !function.isExternal());
|
|
return item;
|
|
}
|
|
|
|
private Map<String, Object> target() {
|
|
Map<String, Object> target = object();
|
|
String executableFormat = currentProgram.getExecutableFormat();
|
|
if (executableFormat.contains("ELF")) {
|
|
target.put("loader", "ElfLoader");
|
|
target.put("format", "ELF");
|
|
} else if (executableFormat.contains("Portable Executable")) {
|
|
target.put("loader", "PeLoader");
|
|
target.put("format",
|
|
currentProgram.getLanguage().getLanguageDescription().getSize() == 64 ? "PE32+" : "PE");
|
|
} else {
|
|
target.put("loader", executableFormat);
|
|
target.put("format", executableFormat);
|
|
}
|
|
target.put("processor_language", currentProgram.getLanguageID().getIdAsString());
|
|
target.put("compiler_specification",
|
|
currentProgram.getCompilerSpec().getCompilerSpecID().getIdAsString());
|
|
return target;
|
|
}
|
|
|
|
private Map<String, Object> queryResult(Object query) {
|
|
Map<String, Object> context = object();
|
|
context.put("ghidra_version", Application.getApplicationVersion());
|
|
context.put("java_version", Runtime.version().feature());
|
|
context.put("target", target());
|
|
context.put("loader_options", List.of());
|
|
context.put("analyzer_options", analysisOptions());
|
|
Map<String, Object> result = object();
|
|
result.put("context", context);
|
|
result.put("query", query);
|
|
return result;
|
|
}
|
|
|
|
private List<Object> analysisOptions() {
|
|
Map<String, String> current = getCurrentAnalysisOptionsAndValues(currentProgram);
|
|
List<String> names = new ArrayList<>(current.keySet());
|
|
names.sort(GhidrAdapter::compareUtf8);
|
|
List<Object> result = new ArrayList<>();
|
|
for (String name : names) {
|
|
Map<String, Object> value = object();
|
|
value.put("type", "string");
|
|
value.put("value", current.get(name));
|
|
Map<String, Object> option = object();
|
|
option.put("name", name);
|
|
option.put("value", value);
|
|
result.add(option);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static Map<String, Object> address(Address value) {
|
|
if (value == null || value == Address.NO_ADDRESS) {
|
|
return null;
|
|
}
|
|
AddressSpace space = value.getAddressSpace();
|
|
int width = Math.max(2, (space.getSize() + 3) / 4);
|
|
String digits = Long.toUnsignedString(value.getOffset(), 16);
|
|
Map<String, Object> result = object();
|
|
result.put("space", space.getName());
|
|
result.put("offset",
|
|
"0x"
|
|
+ "0".repeat(Math.max(0, width - digits.length())) + digits);
|
|
return result;
|
|
}
|
|
|
|
private static Request parseRequest(byte[] bytes) throws AdapterException {
|
|
Object parsed = new JsonParser(decodeUtf8(bytes)).parse();
|
|
Map<String, Object> root = asObject(parsed, "request");
|
|
expectKeys(root,
|
|
Set.of("protocol_version", "invocation_id", "operation", "staged_sample", "analysis_path",
|
|
"limits", "arguments"),
|
|
"request");
|
|
long version = nonNegativeLong(root.get("protocol_version"), "protocol_version");
|
|
if (version != PROTOCOL_VERSION) {
|
|
throw new AdapterException("protocol_violation", "unsupported adapter protocol version");
|
|
}
|
|
String invocation = string(root.get("invocation_id"), "invocation_id");
|
|
if (!invocation.matches("[0-9a-f]{32}")) {
|
|
throw new AdapterException("protocol_violation", "invalid invocation ID");
|
|
}
|
|
String operation = string(root.get("operation"), "operation");
|
|
if (!DISPATCH.contains(operation)) {
|
|
throw new AdapterException("protocol_violation", "operation is not in Java dispatch set");
|
|
}
|
|
Object stagedSample = root.get("staged_sample");
|
|
Object analysisPath = root.get("analysis_path");
|
|
nullableAbsolutePath(stagedSample, "staged_sample");
|
|
nullableAbsolutePath(analysisPath, "analysis_path");
|
|
if (operation.equals("doctor") && (stagedSample != null || analysisPath != null)) {
|
|
throw new AdapterException(
|
|
"protocol_violation", "doctor must not receive Sample or Analysis paths");
|
|
}
|
|
if (!operation.equals("doctor") && (stagedSample == null || analysisPath == null)) {
|
|
throw new AdapterException(
|
|
"protocol_violation", "Sample-backed operation requires Sample and Analysis paths");
|
|
}
|
|
Map<String, Object> limits = asObject(root.get("limits"), "limits");
|
|
expectKeys(limits,
|
|
Set.of("max_heap_mib", "max_cpu", "analysis_timeout_seconds", "decompile_timeout_seconds",
|
|
"child_watchdog_seconds", "max_sample_bytes", "max_inline_bytes"),
|
|
"limits");
|
|
positiveLong(limits.get("max_heap_mib"), "max_heap_mib");
|
|
positiveLong(limits.get("max_cpu"), "max_cpu");
|
|
positiveLong(limits.get("analysis_timeout_seconds"), "analysis_timeout_seconds");
|
|
Object decompileTimeout = limits.get("decompile_timeout_seconds");
|
|
long timeout =
|
|
decompileTimeout == null ? 60 : positiveLong(decompileTimeout, "decompile_timeout_seconds");
|
|
positiveLong(limits.get("child_watchdog_seconds"), "child_watchdog_seconds");
|
|
positiveLong(limits.get("max_sample_bytes"), "max_sample_bytes");
|
|
positiveLong(limits.get("max_inline_bytes"), "max_inline_bytes");
|
|
Map<String, Object> arguments = asObject(root.get("arguments"), "arguments");
|
|
if (!string(arguments.get("kind"), "argument kind").equals(operation)) {
|
|
throw new AdapterException("protocol_violation", "operation and argument kind differ");
|
|
}
|
|
return new Request(invocation, operation, arguments, timeout);
|
|
}
|
|
|
|
private static Map<String, Object> response(
|
|
Request request, String status, Object data, AdapterError error) {
|
|
Map<String, Object> root = object();
|
|
root.put("protocol_version", PROTOCOL_VERSION);
|
|
root.put("invocation_id", request.invocationId);
|
|
root.put("operation", request.operation);
|
|
Map<String, Object> result = object();
|
|
result.put("status", status);
|
|
if (error == null) {
|
|
result.put("data", data);
|
|
} else {
|
|
result.put("code", error.code);
|
|
result.put("message", error.message);
|
|
result.put("details", error.details);
|
|
}
|
|
root.put("result", result);
|
|
return root;
|
|
}
|
|
|
|
private static byte[] readBounded(Path path) throws IOException, AdapterException {
|
|
try (InputStream input = Files.newInputStream(path);
|
|
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
|
byte[] buffer = new byte[8192];
|
|
int remaining = MAX_REQUEST_BYTES + 1;
|
|
while (remaining > 0) {
|
|
int count = input.read(buffer, 0, Math.min(buffer.length, remaining));
|
|
if (count < 0) {
|
|
break;
|
|
}
|
|
output.write(buffer, 0, count);
|
|
remaining -= count;
|
|
}
|
|
if (output.size() > MAX_REQUEST_BYTES) {
|
|
throw new AdapterException("protocol_violation", "request exceeds 1 MiB");
|
|
}
|
|
return output.toByteArray();
|
|
}
|
|
}
|
|
|
|
private static String decodeUtf8(byte[] bytes) throws AdapterException {
|
|
try {
|
|
return StandardCharsets.UTF_8.newDecoder()
|
|
.onMalformedInput(CodingErrorAction.REPORT)
|
|
.onUnmappableCharacter(CodingErrorAction.REPORT)
|
|
.decode(ByteBuffer.wrap(bytes))
|
|
.toString();
|
|
} catch (CharacterCodingException error) {
|
|
throw new AdapterException("protocol_violation", "request is not valid UTF-8");
|
|
}
|
|
}
|
|
|
|
private static void writeResponse(Path temporary, Path response, Object value)
|
|
throws IOException, ResponseTooLargeException {
|
|
Files.deleteIfExists(temporary);
|
|
try (OutputStream raw = Files.newOutputStream(
|
|
temporary, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
|
BoundedOutputStream bounded = new BoundedOutputStream(raw, MAX_RESPONSE_BYTES)) {
|
|
JsonWriter.write(value, bounded);
|
|
bounded.flush();
|
|
} catch (ResponseTooLargeException error) {
|
|
Files.deleteIfExists(temporary);
|
|
throw error;
|
|
}
|
|
try {
|
|
Files.move(temporary, response, StandardCopyOption.ATOMIC_MOVE);
|
|
} catch (AtomicMoveNotSupportedException error) {
|
|
Files.deleteIfExists(temporary);
|
|
throw new IOException("response filesystem does not support atomic rename", error);
|
|
}
|
|
}
|
|
|
|
private void expectProgram() throws AdapterException {
|
|
if (currentProgram == null) {
|
|
throw new AdapterException("analysis_failed", "operation requires an imported Program");
|
|
}
|
|
}
|
|
|
|
private static void requireKind(Map<String, Object> arguments, String expected)
|
|
throws AdapterException {
|
|
if (!string(arguments.get("kind"), "argument kind").equals(expected)) {
|
|
throw new AdapterException("protocol_violation", "unexpected argument kind");
|
|
}
|
|
}
|
|
|
|
private static Long parseLimit(Object value) throws AdapterException {
|
|
if (value instanceof String string && string.equals("all")) {
|
|
return null;
|
|
}
|
|
Map<String, Object> tagged = asObject(value, "limit");
|
|
expectKeys(tagged, Set.of("bounded"), "bounded limit");
|
|
return positiveLong(tagged.get("bounded"), "limit");
|
|
}
|
|
|
|
private static void nullableAbsolutePath(Object value, String name) throws AdapterException {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
String path = string(value, name);
|
|
try {
|
|
if (!Path.of(path).isAbsolute()) {
|
|
throw new AdapterException("protocol_violation", name + " must be absolute");
|
|
}
|
|
} catch (java.nio.file.InvalidPathException error) {
|
|
throw new AdapterException("protocol_violation", name + " is not a valid worker path");
|
|
}
|
|
}
|
|
|
|
private static long positiveLong(Object value, String name) throws AdapterException {
|
|
long result = nonNegativeLong(value, name);
|
|
if (result == 0) {
|
|
throw new AdapterException("protocol_violation", name + " must be positive");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static long nonNegativeLong(Object value, String name) throws AdapterException {
|
|
if (!(value instanceof Long number) || number < 0) {
|
|
throw new AdapterException("protocol_violation", name + " must be a non-negative integer");
|
|
}
|
|
return number;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private static Map<String, Object> asObject(Object value, String name) throws AdapterException {
|
|
if (!(value instanceof Map<?, ?>)) {
|
|
throw new AdapterException("protocol_violation", name + " must be an object");
|
|
}
|
|
return (Map<String, Object>) value;
|
|
}
|
|
|
|
private static String string(Object value, String name) throws AdapterException {
|
|
if (!(value instanceof String text)) {
|
|
throw new AdapterException("protocol_violation", name + " must be a string");
|
|
}
|
|
return text;
|
|
}
|
|
|
|
private static void expectKeys(Map<String, Object> object, Set<String> expected, String name)
|
|
throws AdapterException {
|
|
if (!object.keySet().equals(expected)) {
|
|
throw new AdapterException("protocol_violation", name + " has missing or unknown fields");
|
|
}
|
|
}
|
|
|
|
private static long saturatingAdd(long left, long right) {
|
|
return Long.MAX_VALUE - left < right ? Long.MAX_VALUE : left + right;
|
|
}
|
|
|
|
private static BigInteger unsigned(long value) {
|
|
return new BigInteger(Long.toUnsignedString(value));
|
|
}
|
|
|
|
private static int compareUtf8(String left, String right) {
|
|
byte[] a = left.getBytes(StandardCharsets.UTF_8);
|
|
byte[] b = right.getBytes(StandardCharsets.UTF_8);
|
|
int length = Math.min(a.length, b.length);
|
|
for (int index = 0; index < length; index++) {
|
|
int compared = Integer.compare(Byte.toUnsignedInt(a[index]), Byte.toUnsignedInt(b[index]));
|
|
if (compared != 0) {
|
|
return compared;
|
|
}
|
|
}
|
|
return Integer.compare(a.length, b.length);
|
|
}
|
|
|
|
private static String normalizeLineEndings(String input) {
|
|
return input.replace("\r\n", "\n").replace('\r', '\n');
|
|
}
|
|
|
|
private static Map<String, Object> object() {
|
|
return new LinkedHashMap<>();
|
|
}
|
|
|
|
private record Request(String invocationId, String operation, Map<String, Object> arguments,
|
|
long decompileTimeoutSeconds) {}
|
|
|
|
private record AdapterError(String code, String message, Map<String, Object> details) {}
|
|
|
|
private static final class AdapterException extends Exception {
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
final String code;
|
|
final transient Map<String, Object> details;
|
|
|
|
AdapterException(String code, String message) {
|
|
this(code, message, object());
|
|
}
|
|
|
|
AdapterException(String code, String message, Map<String, Object> details) {
|
|
super(message);
|
|
this.code = code;
|
|
this.details = details;
|
|
}
|
|
}
|
|
|
|
private static final class ResponseTooLargeException extends IOException {
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
final long observedAtLeast;
|
|
|
|
ResponseTooLargeException(long observedAtLeast) {
|
|
super("response exceeds fixed bound");
|
|
this.observedAtLeast = observedAtLeast;
|
|
}
|
|
}
|
|
|
|
private static final class BoundedOutputStream extends OutputStream {
|
|
private final OutputStream delegate;
|
|
private final long limit;
|
|
private long written;
|
|
|
|
BoundedOutputStream(OutputStream delegate, long limit) {
|
|
this.delegate = delegate;
|
|
this.limit = limit;
|
|
}
|
|
|
|
@Override
|
|
public void write(int value) throws IOException {
|
|
ensure(1);
|
|
delegate.write(value);
|
|
written++;
|
|
}
|
|
|
|
@Override
|
|
public void write(byte[] bytes, int offset, int length) throws IOException {
|
|
ensure(length);
|
|
delegate.write(bytes, offset, length);
|
|
written += length;
|
|
}
|
|
|
|
private void ensure(int additional) throws ResponseTooLargeException {
|
|
if (additional > limit - written) {
|
|
throw new ResponseTooLargeException(written + 1);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void flush() throws IOException {
|
|
delegate.flush();
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
delegate.close();
|
|
}
|
|
}
|
|
|
|
private static final class JsonWriter {
|
|
private JsonWriter() {}
|
|
|
|
static void write(Object value, OutputStream output) throws IOException {
|
|
if (value == null) {
|
|
bytes(output, "null");
|
|
} else if (value instanceof String string) {
|
|
string(output, string);
|
|
} else if (value instanceof Boolean || value instanceof Number) {
|
|
bytes(output, value.toString());
|
|
} else if (value instanceof Map<?, ?> map) {
|
|
output.write('{');
|
|
boolean first = true;
|
|
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
|
if (!first) {
|
|
output.write(',');
|
|
}
|
|
first = false;
|
|
string(output, entry.getKey().toString());
|
|
output.write(':');
|
|
write(entry.getValue(), output);
|
|
}
|
|
output.write('}');
|
|
} else if (value instanceof Iterable<?> iterable) {
|
|
output.write('[');
|
|
boolean first = true;
|
|
for (Object item : iterable) {
|
|
if (!first) {
|
|
output.write(',');
|
|
}
|
|
first = false;
|
|
write(item, output);
|
|
}
|
|
output.write(']');
|
|
} else {
|
|
throw new IOException("unsupported JSON value");
|
|
}
|
|
}
|
|
|
|
private static void string(OutputStream output, String value) throws IOException {
|
|
output.write('"');
|
|
for (int index = 0; index < value.length(); index++) {
|
|
char character = value.charAt(index);
|
|
switch (character) {
|
|
case '"' -> bytes(output, "\\\"");
|
|
case '\\' -> bytes(output, "\\\\");
|
|
case '\b' -> bytes(output, "\\b");
|
|
case '\f' -> bytes(output, "\\f");
|
|
case '\n' -> bytes(output, "\\n");
|
|
case '\r' -> bytes(output, "\\r");
|
|
case '\t' -> bytes(output, "\\t");
|
|
default -> {
|
|
if (character < 0x20) {
|
|
bytes(output, String.format("\\u%04x", (int) character));
|
|
} else {
|
|
int codePoint = value.codePointAt(index);
|
|
byte[] encoded =
|
|
new String(Character.toChars(codePoint)).getBytes(StandardCharsets.UTF_8);
|
|
output.write(encoded);
|
|
if (Character.isSupplementaryCodePoint(codePoint)) {
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
output.write('"');
|
|
}
|
|
|
|
private static void bytes(OutputStream output, String value) throws IOException {
|
|
output.write(value.getBytes(StandardCharsets.UTF_8));
|
|
}
|
|
}
|
|
|
|
private static final class JsonParser {
|
|
private final String input;
|
|
private int offset;
|
|
|
|
JsonParser(String input) {
|
|
this.input = input;
|
|
}
|
|
|
|
Object parse() throws AdapterException {
|
|
Object value = value();
|
|
whitespace();
|
|
if (offset != input.length()) {
|
|
fail();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private Object value() throws AdapterException {
|
|
whitespace();
|
|
if (offset >= input.length()) {
|
|
return fail();
|
|
}
|
|
return switch (input.charAt(offset)) {
|
|
case '{' -> objectValue();
|
|
case '[' -> arrayValue();
|
|
case '"' -> stringValue();
|
|
case 't' -> literal("true", Boolean.TRUE);
|
|
case 'f' -> literal("false", Boolean.FALSE);
|
|
case 'n' -> literal("null", null);
|
|
default -> numberValue();
|
|
};
|
|
}
|
|
|
|
private Map<String, Object> objectValue() throws AdapterException {
|
|
offset++;
|
|
Map<String, Object> result = object();
|
|
whitespace();
|
|
if (consume('}')) {
|
|
return result;
|
|
}
|
|
while (true) {
|
|
whitespace();
|
|
if (offset >= input.length() || input.charAt(offset) != '"') {
|
|
return fail();
|
|
}
|
|
String key = stringValue();
|
|
if (result.containsKey(key)) {
|
|
throw new AdapterException("protocol_violation", "duplicate JSON object key");
|
|
}
|
|
whitespace();
|
|
if (!consume(':')) {
|
|
return fail();
|
|
}
|
|
result.put(key, value());
|
|
whitespace();
|
|
if (consume('}')) {
|
|
return result;
|
|
}
|
|
if (!consume(',')) {
|
|
return fail();
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<Object> arrayValue() throws AdapterException {
|
|
offset++;
|
|
List<Object> result = new ArrayList<>();
|
|
whitespace();
|
|
if (consume(']')) {
|
|
return result;
|
|
}
|
|
while (true) {
|
|
result.add(value());
|
|
whitespace();
|
|
if (consume(']')) {
|
|
return result;
|
|
}
|
|
if (!consume(',')) {
|
|
return fail();
|
|
}
|
|
}
|
|
}
|
|
|
|
private String stringValue() throws AdapterException {
|
|
offset++;
|
|
StringBuilder result = new StringBuilder();
|
|
while (offset < input.length()) {
|
|
char character = input.charAt(offset++);
|
|
if (character == '"') {
|
|
return validString(result.toString());
|
|
}
|
|
if (character == '\\') {
|
|
if (offset >= input.length()) {
|
|
return fail();
|
|
}
|
|
char escaped = input.charAt(offset++);
|
|
switch (escaped) {
|
|
case '"', '\\', '/' -> result.append(escaped);
|
|
case 'b' -> result.append('\b');
|
|
case 'f' -> result.append('\f');
|
|
case 'n' -> result.append('\n');
|
|
case 'r' -> result.append('\r');
|
|
case 't' -> result.append('\t');
|
|
case 'u' -> result.append(unicodeEscape());
|
|
default -> throw new AdapterException("protocol_violation", "invalid JSON escape");
|
|
}
|
|
} else if (character < 0x20) {
|
|
return fail();
|
|
} else {
|
|
result.append(character);
|
|
}
|
|
}
|
|
return fail();
|
|
}
|
|
|
|
private String validString(String value) throws AdapterException {
|
|
for (int index = 0; index < value.length(); index++) {
|
|
char character = value.charAt(index);
|
|
if (Character.isHighSurrogate(character)) {
|
|
if (index + 1 >= value.length() || !Character.isLowSurrogate(value.charAt(index + 1))) {
|
|
throw new AdapterException(
|
|
"protocol_violation", "JSON string has an unpaired surrogate");
|
|
}
|
|
index++;
|
|
} else if (Character.isLowSurrogate(character)) {
|
|
throw new AdapterException("protocol_violation", "JSON string has an unpaired surrogate");
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private char unicodeEscape() throws AdapterException {
|
|
if (offset + 4 > input.length()) {
|
|
return fail();
|
|
}
|
|
try {
|
|
char value = (char) Integer.parseInt(input.substring(offset, offset + 4), 16);
|
|
offset += 4;
|
|
return value;
|
|
} catch (NumberFormatException error) {
|
|
return fail();
|
|
}
|
|
}
|
|
|
|
private Object numberValue() throws AdapterException {
|
|
int start = offset;
|
|
if (offset < input.length() && input.charAt(offset) == '-') {
|
|
offset++;
|
|
}
|
|
if (offset >= input.length() || !Character.isDigit(input.charAt(offset))) {
|
|
return fail();
|
|
}
|
|
if (input.charAt(offset) == '0') {
|
|
offset++;
|
|
} else {
|
|
while (offset < input.length() && Character.isDigit(input.charAt(offset))) {
|
|
offset++;
|
|
}
|
|
}
|
|
if (offset < input.length() && ".eE+".indexOf(input.charAt(offset)) >= 0) {
|
|
throw new AdapterException("protocol_violation", "floating-point JSON is not accepted");
|
|
}
|
|
try {
|
|
return Long.valueOf(input.substring(start, offset));
|
|
} catch (NumberFormatException error) {
|
|
throw new AdapterException("protocol_violation", "JSON integer is out of range");
|
|
}
|
|
}
|
|
|
|
private Object literal(String text, Object value) throws AdapterException {
|
|
if (!input.startsWith(text, offset)) {
|
|
return fail();
|
|
}
|
|
offset += text.length();
|
|
return value;
|
|
}
|
|
|
|
private boolean consume(char expected) {
|
|
if (offset < input.length() && input.charAt(offset) == expected) {
|
|
offset++;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void whitespace() {
|
|
while (offset < input.length() && " \n\r\t".indexOf(input.charAt(offset)) >= 0) {
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
private <T> T fail() throws AdapterException {
|
|
throw new AdapterException("protocol_violation", "malformed JSON request");
|
|
}
|
|
}
|
|
}
|