// 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.script.GhidraScript; 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 GhidraScript { 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 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)); Object data = dispatch(request); writeResponse(temporaryPath, responsePath, response(request, "success", data, null)); } catch (ResponseTooLargeException error) { Files.deleteIfExists(temporaryPath); if (request == null) { throw error; } Map 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 arguments) throws AdapterException { expectKeys(arguments, Set.of("kind"), "doctor arguments"); requireKind(arguments, "doctor"); Map 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 arguments) throws AdapterException { expectProgram(); expectKeys(arguments, Set.of("kind"), "inspect arguments"); requireKind(arguments, "inspect"); Map 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()); Map result = object(); result.put("program", program); result.put("target", target()); result.put("ghidra_version", Application.getApplicationVersion()); result.put("java_version", Runtime.version().feature()); return result; } private Object functions(Map arguments) throws AdapterException { expectProgram(); expectKeys(arguments, Set.of("kind", "page"), "functions arguments"); requireKind(arguments, "functions"); Map 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 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 items = new ArrayList<>(); for (Function function : all.subList(start, end)) { items.add(functionItem(function)); } Map 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 result = object(); result.put("page", page); result.put("items", items); return result; } private Object decompile(Map arguments, long timeoutSeconds) throws Exception { expectProgram(); expectKeys(arguments, Set.of("kind", "selector"), "decompile arguments"); requireKind(arguments, "decompile"); Map 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 selected = object(); selected.put("name", function.getName(false)); selected.put("qualified_name", function.getName(true)); selected.put("entry", address(function.getEntryPoint())); Map decompilation = object(); decompilation.put("syntax", "ghidra_c"); decompilation.put("text", text); Map result = object(); result.put("requested_selector", selector); result.put("function", selected); result.put("decompilation", decompilation); return result; } private Function resolveName(String requested) throws AdapterException { boolean qualified = requested.contains("::"); List 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 details = object(); List 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 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 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 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 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 allFunctions() { LinkedHashSet 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 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 functionItem(Function function) { Map 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 target() { Map target = object(); target.put("loader", currentProgram.getExecutableFormat()); target.put("format", currentProgram.getExecutableFormat()); target.put("processor_language", currentProgram.getLanguageID().getIdAsString()); target.put("compiler_specification", currentProgram.getCompilerSpec().getCompilerSpecID().getIdAsString()); return target; } private static Map 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 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 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"); } nullableAbsolutePath(root.get("staged_sample"), "staged_sample"); nullableAbsolutePath(root.get("analysis_path"), "analysis_path"); Map 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 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 response( Request request, String status, Object data, AdapterError error) { Map root = object(); root.put("protocol_version", PROTOCOL_VERSION); root.put("invocation_id", request.invocationId); root.put("operation", request.operation); Map 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 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 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 asObject(Object value, String name) throws AdapterException { if (!(value instanceof Map)) { throw new AdapterException("protocol_violation", name + " must be an object"); } return (Map) 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 object, Set 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 object() { return new LinkedHashMap<>(); } private record Request(String invocationId, String operation, Map arguments, long decompileTimeoutSeconds) {} private record AdapterError(String code, String message, Map details) {} private static final class AdapterException extends Exception { private static final long serialVersionUID = 1L; final String code; final Map details; AdapterException(String code, String message) { this(code, message, object()); } AdapterException(String code, String message, Map 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 objectValue() throws AdapterException { offset++; Map 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 arrayValue() throws AdapterException { offset++; List 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 fail() throws AdapterException { throw new AdapterException("protocol_violation", "malformed JSON request"); } } }