From c21f399b699080502cf423c4e0269433d8fc7104 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sun, 23 Aug 2026 10:41:49 +0900 Subject: [PATCH] WIP --- .../swiss/qpq/gajumaru/core/encoding/ZJ.java | 94 +++++++++++++------ .../swiss/qpq/gajumaru/core/tools/Grids.java | 17 ++-- 2 files changed, 71 insertions(+), 40 deletions(-) diff --git a/src/main/java/swiss/qpq/gajumaru/core/encoding/ZJ.java b/src/main/java/swiss/qpq/gajumaru/core/encoding/ZJ.java index d56e4f5..9072abe 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/encoding/ZJ.java +++ b/src/main/java/swiss/qpq/gajumaru/core/encoding/ZJ.java @@ -1,21 +1,28 @@ package swiss.qpq.gajumaru.core.encoding; +import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; // ZJ: Tiny JSON parser, ported from zj.erl. // // NOTE: -// Does not *quite* live up to ZJ in every respect, but is sufficient for GRIDS (maybe). -// The most important difference is that this version makes use of `throw`. A lot. -// That sucks, but I don't know a clean way to get the error tuple return style yet. - +// Does not *quite* live up to ZJ in every respect, but is sufficient for GRIDS. +// Uses standard Java exceptions for errors. public final class ZJ { + public static class ParseException extends RuntimeException { + public final int position; + public ParseException(String message, int position) { + super(message + " at position " + position); + this.position = position; + } + } + private ZJ() {} public static Object decode(String json) { @@ -47,10 +54,7 @@ public final class ZJ { } else if (value instanceof Object[]) { encodeArray((Object[]) value, sb); } else { - // Fallback for other objects, though not strictly in ZJ - sb.append('"'); - escape(value.toString(), sb); - sb.append('"'); + throw new IllegalArgumentException("Unsupported JSON value type: " + value.getClass().getName()); } } @@ -123,7 +127,12 @@ public final class ZJ { Object parse() { seek(); if (pos >= json.length()) return null; - return value(); + Object result = value(); + seek(); + if (pos != json.length()) { + throw new ParseException("Unexpected trailing data", pos); + } + return result; } private Object value() { @@ -139,14 +148,14 @@ public final class ZJ { if (c == '-' || (c >= '0' && c <= '9')) { yield number(); } - throw new RuntimeException("Unexpected character at " + pos + ": " + c); + throw new ParseException("Unexpected character: " + c, pos); } }; } private Map object() { pos++; // skip '{' - Map map = new HashMap<>(); + Map map = new LinkedHashMap<>(); seek(); if (pos < json.length() && json.charAt(pos) == '}') { pos++; @@ -157,7 +166,7 @@ public final class ZJ { String key = string(); seek(); if (pos >= json.length() || json.charAt(pos) != ':') { - throw new RuntimeException("Expected ':' at " + pos); + throw new ParseException("Expected ':'", pos); } pos++; seek(); @@ -169,7 +178,7 @@ public final class ZJ { break; } if (pos >= json.length() || json.charAt(pos) != ',') { - throw new RuntimeException("Expected ',' or '}' at " + pos); + throw new ParseException("Expected ',' or '}'", pos); } pos++; } @@ -193,7 +202,7 @@ public final class ZJ { break; } if (pos >= json.length() || json.charAt(pos) != ',') { - throw new RuntimeException("Expected ',' or ']' at " + pos); + throw new ParseException("Expected ',' or ']'", pos); } pos++; } @@ -201,14 +210,15 @@ public final class ZJ { } private String string() { - if (json.charAt(pos) != '"') throw new RuntimeException("Expected '\"' at " + pos); + if (json.charAt(pos) != '"') throw new ParseException("Expected '\"'", pos); pos++; StringBuilder sb = new StringBuilder(); while (pos < json.length()) { char c = json.charAt(pos++); if (c == '"') return sb.toString(); + if (c < 0x20) throw new ParseException("Unescaped control character", pos - 1); if (c == '\\') { - if (pos >= json.length()) throw new RuntimeException("Unterminated escape at " + pos); + if (pos >= json.length()) throw new ParseException("Unterminated escape", pos); char esc = json.charAt(pos++); switch (esc) { case '"' -> sb.append('"'); @@ -220,18 +230,22 @@ public final class ZJ { case 'r' -> sb.append('\r'); case 't' -> sb.append('\t'); case 'u' -> { - if (pos + 4 > json.length()) throw new RuntimeException("Invalid unicode escape"); + if (pos + 4 > json.length()) throw new ParseException("Invalid unicode escape", pos); String hex = json.substring(pos, pos + 4); - sb.append((char) Integer.parseInt(hex, 16)); + try { + sb.append((char) Integer.parseInt(hex, 16)); + } catch (NumberFormatException e) { + throw new ParseException("Invalid hex in unicode escape", pos); + } pos += 4; } - default -> throw new RuntimeException("Unknown escape: " + esc); + default -> throw new ParseException("Unknown escape: " + esc, pos - 1); } } else { sb.append(c); } } - throw new RuntimeException("Unterminated string"); + throw new ParseException("Unterminated string", pos); } private Boolean bool(boolean expected) { @@ -240,7 +254,7 @@ public final class ZJ { pos += s.length(); return expected; } - throw new RuntimeException("Expected " + s + " at " + pos); + throw new ParseException("Expected " + s, pos); } private Object nil() { @@ -248,27 +262,47 @@ public final class ZJ { pos += 4; return null; } - throw new RuntimeException("Expected null at " + pos); + throw new ParseException("Expected null", pos); } private Number number() { int start = pos; - if (json.charAt(pos) == '-') pos++; - while (pos < json.length() && Character.isDigit(json.charAt(pos))) pos++; - boolean isFloat = false; - if (pos < json.length() && json.charAt(pos) == '.') { - isFloat = true; + if (pos < json.length() && json.charAt(pos) == '-') pos++; + + if (pos >= json.length()) throw new ParseException("Unexpected end of number", pos); + + char c = json.charAt(pos); + if (c == '0') { + pos++; + } else if (c >= '1' && c <= '9') { pos++; while (pos < json.length() && Character.isDigit(json.charAt(pos))) pos++; + } else { + throw new ParseException("Expected digit", pos); } + + boolean isDecimal = false; + if (pos < json.length() && json.charAt(pos) == '.') { + isDecimal = true; + pos++; + if (pos >= json.length() || !Character.isDigit(json.charAt(pos))) { + throw new ParseException("Expected digit after '.'", pos); + } + while (pos < json.length() && Character.isDigit(json.charAt(pos))) pos++; + } + if (pos < json.length() && (json.charAt(pos) == 'e' || json.charAt(pos) == 'E')) { - isFloat = true; + isDecimal = true; pos++; if (pos < json.length() && (json.charAt(pos) == '+' || json.charAt(pos) == '-')) pos++; + if (pos >= json.length() || !Character.isDigit(json.charAt(pos))) { + throw new ParseException("Expected digit in exponent", pos); + } while (pos < json.length() && Character.isDigit(json.charAt(pos))) pos++; } + String s = json.substring(start, pos); - if (isFloat) return Double.parseDouble(s); + if (isDecimal) return new BigDecimal(s); try { return Long.parseLong(s); } catch (NumberFormatException e) { diff --git a/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java b/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java index d51fa35..a5984a0 100644 --- a/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java +++ b/src/main/java/swiss/qpq/gajumaru/core/tools/Grids.java @@ -4,12 +4,11 @@ import java.math.BigInteger; import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; -/** - * GRIDS URL handling, ported from hz_grids.erl. - */ +// GRIDS URL handling, ported from hz_grids.erl. + public final class Grids { private Grids() {} @@ -87,8 +86,8 @@ public final class Grids { } else if (path.startsWith("/1/d/")) { String subPath = path.substring(5); String httpScheme = "grids".equals(scheme) ? "https" : "http"; - String httpUrl = httpScheme + "://" + host + (uri.getPort() != -1 ? ":" + uri.getPort() : "") + subPath; - if (query != null) httpUrl += "?" + query; + String baseUrl = httpScheme + "://" + host + (uri.getPort() != -1 ? ":" + uri.getPort() : "") + subPath; + String httpUrl = query != null ? baseUrl + "?" + query : baseUrl; return ParseResult.sign("grids".equals(scheme) ? Context.HTTPS : Context.HTTP, httpUrl); } else { throw new IllegalArgumentException("Unknown verb in path: " + path); @@ -103,7 +102,7 @@ public final class Grids { public static final String REQ_TYPE_ACK = "ack"; public static Map makeRequest(String type, Object payload, String publicId, String networkId) { - Map req = new HashMap<>(); + Map req = new LinkedHashMap<>(); req.put("grids", 1); req.put("chain", "gajumaru"); req.put("network_id", networkId); @@ -114,7 +113,7 @@ public final class Grids { } private static Map parseQuery(String query) { - Map params = new HashMap<>(); + Map params = new LinkedHashMap<>(); if (query == null || query.isEmpty()) return params; String[] pairs = query.split("&"); for (String pair : pairs) { @@ -133,8 +132,6 @@ public final class Grids { if (p == null || p.isEmpty()) return new byte[0]; // The Erlang side uses uri_string:compose_query which percent-encodes. // URLDecoder.decode should have handled it in parseQuery. - // Assuming payload is just string data or hex? - // hz_grids.erl says: Payload = list_to_binary(proplists:get_value("p", ArgList, "")) return p.getBytes(StandardCharsets.UTF_8); } }