This commit is contained in:
2026-08-23 10:41:49 +09:00
parent 97bb833d35
commit c21f399b69
2 changed files with 71 additions and 40 deletions
@@ -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<String, Object> object() {
pos++; // skip '{'
Map<String, Object> map = new HashMap<>();
Map<String, Object> 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) {
@@ -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<String, Object> makeRequest(String type, Object payload, String publicId, String networkId) {
Map<String, Object> req = new HashMap<>();
Map<String, Object> 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<String, String> parseQuery(String query) {
Map<String, String> params = new HashMap<>();
Map<String, String> 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);
}
}