001package gudusoft.gsqlparser.slpc.json; 002 003import gudusoft.gsqlparser.slpc.model.SlpcDocument; 004import gudusoft.gsqlparser.slpc.model.SlpcObject; 005 006import java.io.ByteArrayOutputStream; 007import java.math.BigInteger; 008import java.nio.ByteBuffer; 009import java.nio.charset.CharacterCodingException; 010import java.nio.charset.CodingErrorAction; 011import java.nio.charset.StandardCharsets; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collections; 015import java.util.Comparator; 016import java.util.List; 017 018/** Strict reader and deterministic UTF-8 writer for SLPC JSON. */ 019public final class SlpcJson { 020 021 private SlpcJson() { 022 } 023 024 public static SlpcDocument readDocument(byte[] utf8) { 025 Object value = new Parser(decodeUtf8(utf8)).parse(); 026 if (!(value instanceof SlpcObject)) { 027 throw new SlpcJsonException("SLPC document root must be an object"); 028 } 029 return new SlpcDocument((SlpcObject) value); 030 } 031 032 public static SlpcObject readObject(byte[] utf8) { 033 Object value = new Parser(decodeUtf8(utf8)).parse(); 034 if (!(value instanceof SlpcObject)) { 035 throw new SlpcJsonException("JSON root must be an object"); 036 } 037 return (SlpcObject) value; 038 } 039 040 public static byte[] canonicalBytes(SlpcDocument document) { 041 return canonicalBytes(document.root()); 042 } 043 044 public static byte[] canonicalBytes(Object value) { 045 ByteArrayOutputStream output = new ByteArrayOutputStream(); 046 write(value, output); 047 return output.toByteArray(); 048 } 049 050 public static String canonicalString(Object value) { 051 return new String(canonicalBytes(value), StandardCharsets.UTF_8); 052 } 053 054 private static String decodeUtf8(byte[] value) { 055 try { 056 return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT) 057 .onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(value)).toString(); 058 } catch (CharacterCodingException invalid) { 059 throw new SlpcJsonException("Input is not well-formed UTF-8"); 060 } 061 } 062 063 private static void write(Object value, ByteArrayOutputStream output) { 064 write(value, output, false); 065 } 066 067 private static void write(Object value, ByteArrayOutputStream output, boolean extensionPayload) { 068 if (value == null) { 069 ascii(output, "null"); 070 } else if (value instanceof String) { 071 writeString((String) value, output); 072 } else if (value instanceof Boolean || value instanceof BigInteger) { 073 ascii(output, value.toString()); 074 } else if (value instanceof SlpcObject) { 075 writeObject((SlpcObject) value, output, extensionPayload); 076 } else if (value instanceof List<?>) { 077 output.write('['); 078 boolean first = true; 079 for (Object item : (List<?>) value) { 080 if (!first) { 081 output.write(','); 082 } 083 write(item, output, extensionPayload); 084 first = false; 085 } 086 output.write(']'); 087 } else if (value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long) { 088 ascii(output, String.valueOf(value)); 089 } else { 090 throw new SlpcJsonException("Unsupported JSON value type: " + value.getClass().getName()); 091 } 092 } 093 094 private static void writeObject(final SlpcObject object, ByteArrayOutputStream output, boolean extensionPayload) { 095 List<String> names = new ArrayList<String>(object.names()); 096 final List<String> schemaOrder = extensionPayload ? null : schemaFieldOrder(object); 097 if (schemaOrder == null) { 098 Collections.sort(names, UTF8_COMPARATOR); 099 } else { 100 Collections.sort(names, new Comparator<String>() { @Override public int compare(String left, String right) { 101 int leftIndex = schemaOrder.indexOf(left), rightIndex = schemaOrder.indexOf(right); 102 if (leftIndex < 0 && rightIndex < 0) return UTF8_COMPARATOR.compare(left, right); 103 if (leftIndex < 0) return 1; 104 if (rightIndex < 0) return -1; 105 return leftIndex - rightIndex; 106 }}); 107 } 108 output.write('{'); 109 boolean first = true; 110 for (String name : names) { 111 if (!first) { 112 output.write(','); 113 } 114 writeString(name, output); 115 output.write(':'); 116 boolean payload = extensionPayload || ("payload".equals(name) && object.contains("extensionType")); 117 write(object.get(name), output, payload); 118 first = false; 119 } 120 output.write('}'); 121 } 122 123 /** Package-visible so the conformance test can detect schema/order drift. */ 124 /** 125 * The canonical property order for the given SLPC object shape. 126 * 127 * <p>PUBLIC ON PURPOSE: every shipped jar is ProGuard-obfuscated under a 128 * compatibility-first policy that preserves public and protected members 129 * and renames everything else. A package-private method is therefore 130 * unreachable from the released jar, which is what the test suite runs 131 * against in the nightly obfuscated-jar gate. Keeping this public is what 132 * lets that gate execute the real shipped bytes.</p> 133 * 134 * @since 4.2.6 135 */ 136 public static List<String> schemaFieldOrder(SlpcObject value) { 137 if (value.contains("contractVersion")) return fields("contractVersion", "identitySpecVersion", "producer", "artifacts", "statements", "endpoints", "facts", "factCandidates", "datasetOperationInfluences", "datasetOperationCandidates", "occurrences", "diagnostics", "extensions", "objectRelationships", "objectRelationshipCandidates", "statementOperations", "joinSemantics", "intermediateGraphs"); 138 if (value.contains("reportVersion")) return fields("reportVersion", "projectorName", "projectorVersion", "targetType", "inputCounts", "outcomeCounts", "records", "diagnosticCounts"); 139 if (value.contains("engineVersion") && value.contains("capabilities")) return fields("engineKind", "engineVersion", "adapterName", "adapterVersion", "capabilities"); 140 if (value.contains("code") && value.contains("status")) return fields("code", "status"); 141 if (value.contains("artifactId") && value.contains("sourceKind")) return fields("artifactId", "sourceKind", "logicalLocator", "sqlDialect", "contentHash", "mediaType"); 142 if (value.contains("statementIndex")) return fields("statementId", "artifactId", "statementIndex", "kind", "queryFingerprint", "sourceLocation"); 143 if (value.contains("endpointLevel")) return fields("endpointRef", "canonicalEndpointId", "endpointLevel", "objectKind", "displaySegments", "identity", "resolutionStatus", "publicationState", "scope", "extensions"); 144 if (value.contains("display") && value.contains("inputForm")) return fields("kind", "display", "inputForm", "provenance"); 145 if (value.contains("strength")) { 146 String kind = value.string("kind"); 147 if ("COMPOSITE_EXACT".equals(kind) || "COMPOSITE_APPROXIMATE".equals(kind)) return fields("kind", "strength", "qualificationPolicyId", "segments"); 148 if ("AUTHORITATIVE_EXTERNAL".equals(kind)) return fields("kind", "strength", "system", "identityVersion", "opaqueId"); 149 if ("URI_EXACT".equals(kind)) return fields("kind", "strength", "normalizationPolicyId", "scheme", "locatorDigest"); 150 return fields("kind", "strength", "reason"); 151 } 152 if (value.contains("segmentKind")) { 153 if (value.contains("payload")) return fields("segmentKind", "identityKind", "policyId", "objectGroup", "payload"); 154 return fields("segmentKind", "identityKind", "system", "identityVersion", "opaqueId"); 155 } 156 if (value.contains("scopeKind")) return fields("scopeKind", "scopeId", "definedAtStatementId"); 157 if (value.contains("factId")) return fields("factId", "sourceEndpointRef", "targetEndpointRef", "axis", "role", "operationKind", "operationInputKind", "qualitySummary", "extensions"); 158 if (value.contains("candidateFingerprint") && value.contains("axis")) return fields("candidateId", "candidateFingerprint", "sourceEndpointRef", "targetEndpointRef", "axis", "role", "operationKind", "operationInputKind", "candidateReasons", "qualitySummary", "extensions"); 159 if (value.contains("influenceId")) return fields("influenceId", "sourceEndpointRef", "targetEndpointRef", "operationKind", "qualitySummary", "extensions"); 160 if (value.contains("candidateFingerprint") && value.contains("sourceEndpointRef")) return fields("candidateId", "candidateFingerprint", "sourceEndpointRef", "targetEndpointRef", "operationKind", "candidateReasons", "qualitySummary", "extensions"); 161 if (value.contains("relationshipId")) return fields("relationshipId", "relationshipKind", "participants", "keyFields", "keyPairs", "qualitySummary", "extensions"); 162 if (value.contains("candidateFingerprint") && value.contains("relationshipKind")) return fields("candidateId", "candidateFingerprint", "relationshipKind", "participants", "keyFields", "keyPairs", "candidateReasons", "qualitySummary", "extensions"); 163 if (value.contains("foreignKeyFieldRef")) return fields("ordinal", "foreignKeyFieldRef", "referencedFieldRef"); 164 if (value.contains("fieldEndpointRef")) return fields("ordinal", "fieldEndpointRef"); 165 if (value.contains("statementOperationId")) return fields("statementOperationId", "statementId", "operationOrdinal", "operationKind", "participants", "contentSemantics", "quality", "sourceLocations", "extensions"); 166 if (value.contains("joinSemanticsId")) return fields("joinSemanticsId", "statementId", "joinOrdinal", "joinType", "conditionForm", "leftRelationEndpointRef", "rightRelationEndpointRef", "leftSideSemantics", "rightSideSemantics", "keyPairs", "predicate", "refinesFactIds", "refinesFactCandidateFingerprints", "quality", "sourceLocations", "extensions"); 167 if (value.contains("leftFieldEndpointRef")) return fields("ordinal", "leftFieldEndpointRef", "rightFieldEndpointRef", "operator"); 168 if (value.contains("graphFingerprint")) return fields("graphRef", "graphFingerprint", "statementId", "graphOrdinal", "graphKind", "analysisCompleteness", "nodes", "edges", "paths", "extensions"); 169 if (value.contains("nodeFingerprint")) return fields("nodeRef", "nodeFingerprint", "nodeOrdinal", "nodeKind", "localName", "parentNodeRef", "boundaryEndpointRef", "nodeEvidence", "extensions"); 170 if (value.contains("edgeFingerprint")) return fields("edgeRef", "edgeFingerprint", "source", "target", "axis", "role", "operationKind", "operationInputKind", "transformation", "predicate", "extensions"); 171 if (value.contains("pathFingerprint")) return fields("pathRef", "pathFingerprint", "edgeRefs", "extensions"); 172 if (value.contains("stepFingerprint")) return fields("stepRef", "stepFingerprint", "stepOrdinal", "operationFamily", "operationCode", "inputBindings", "resultAnchor", "expressionEvidence", "extensions"); 173 if (value.contains("roleCode")) return fields("ordinal", "roleCode", "edgeRef"); 174 if (value.contains("occurrenceId")) return fields("occurrenceId", "ownerRef", "artifactId", "statementId", "evidenceKind", "origin", "procedureEndpointRef", "procedureCanonicalEndpointId", "transformation", "predicate", "quality", "sourceLocations", "rowImpactDetails", "literalInputs", "copyClassifications", "supportPathRefs", "extensions"); 175 if (value.contains("engineKind") && value.contains("adapterName")) return fields("engineKind", "adapterName"); 176 if (value.contains("fingerprint") && value.contains("locationNullReason")) return fields("text", "fingerprint", "sourceLocation", "locationNullReason"); 177 if (value.contains("clauseKind")) return fields("clauseKind", "semanticScope", "projectionFieldSetKind", "projectedFieldEndpointRefs", "projectionCompleteness"); 178 if (value.contains("literalKind")) return fields("inputOrdinal", "literalKind", "text", "redactionOrdinal", "fingerprint", "sourceLocation", "locationNullReason"); 179 if (value.contains("classification")) return fields("classification", "provenance", "projectorProfileCode"); 180 if (value.contains("graphRef") && value.contains("pathRef")) return fields("graphRef", "pathRef"); 181 if (value.contains("resolutionGrade")) { 182 if (value.contains("projectionEligibility")) return fields("projectionEligibility", "resolutionGrade", "evaluationModel", "semanticProof", "dynamic", "confidenceMicros", "evidenceCount"); 183 return fields("resolutionGrade", "evaluationModel", "semanticProof", "dynamic", "confidenceMicros"); 184 } 185 if (value.contains("severity") && value.contains("scope")) return fields("code", "severity", "scope", "ref", "message", "sourceLocation", "extensions"); 186 if (value.contains("extensionType")) return fields("extensionType", "schemaVersion", "criticality", "payload"); 187 if (value.contains("STANDARD") && value.contains("UNSUPPORTED")) return fields("STANDARD", "EXTENSION_ONLY", "CONDITIONAL", "INELIGIBLE", "UNSUPPORTED"); 188 if (value.contains("facts") && value.contains("total")) return fields("facts", "factCandidates", "datasetOperationInfluences", "datasetOperationCandidates", "objectRelationships", "objectRelationshipCandidates", "statementOperations", "joinSemantics", "intermediateGraphs", "total"); 189 if (value.contains("outcome") && value.contains("standardOutputCount")) return fields("ownerRef", "outcome", "standardOutputCount", "extensionTypes", "diagnosticCodes"); 190 if (value.contains("count") && value.contains("code")) return fields("code", "count"); 191 if (value.contains("startLine")) return fields("artifactId", "statementId", "startLine", "startColumn", "endLine", "endColumn"); 192 if (value.contains("kind") && value.contains("portCode")) { 193 return "ENDPOINT".equals(value.string("kind")) ? fields("kind", "endpointRef", "portCode") : fields("kind", "nodeRef", "portCode"); 194 } 195 if (value.contains("kind") && value.contains("id")) return fields("kind", "id"); 196 if (value.contains("role") && value.contains("endpointRef")) return fields("role", "endpointRef"); 197 if (value.contains("algorithm") && value.contains("value")) return fields("algorithm", "value"); 198 if (value.contains("steps")) return fields("steps"); 199 return null; 200 } 201 202 private static List<String> fields(String... names) { 203 return Arrays.asList(names); 204 } 205 206 private static void writeString(String value, ByteArrayOutputStream output) { 207 rejectUnpairedSurrogates(value); 208 output.write('"'); 209 for (int index = 0; index < value.length(); index++) { 210 char character = value.charAt(index); 211 switch (character) { 212 case '"': ascii(output, "\\\""); break; 213 case '\\': ascii(output, "\\\\"); break; 214 case '\b': ascii(output, "\\b"); break; 215 case '\f': ascii(output, "\\f"); break; 216 case '\n': ascii(output, "\\n"); break; 217 case '\r': ascii(output, "\\r"); break; 218 case '\t': ascii(output, "\\t"); break; 219 default: 220 if (character < 0x20) { 221 ascii(output, String.format("\\u%04x", (int) character)); 222 } else { 223 int codePoint = Character.codePointAt(value, index); 224 byte[] bytes = new String(Character.toChars(codePoint)).getBytes(StandardCharsets.UTF_8); 225 output.write(bytes, 0, bytes.length); 226 if (Character.isSupplementaryCodePoint(codePoint)) { 227 index++; 228 } 229 } 230 } 231 } 232 output.write('"'); 233 } 234 235 private static void ascii(ByteArrayOutputStream output, String value) { 236 byte[] bytes = value.getBytes(StandardCharsets.US_ASCII); 237 output.write(bytes, 0, bytes.length); 238 } 239 240 private static void rejectUnpairedSurrogates(String value) { 241 for (int index = 0; index < value.length(); index++) { 242 char character = value.charAt(index); 243 if (Character.isHighSurrogate(character)) { 244 if (index + 1 >= value.length() || !Character.isLowSurrogate(value.charAt(index + 1))) { 245 throw new SlpcJsonException("Unpaired high surrogate in JSON string"); 246 } 247 index++; 248 } else if (Character.isLowSurrogate(character)) { 249 throw new SlpcJsonException("Unpaired low surrogate in JSON string"); 250 } 251 } 252 } 253 254 private static final Comparator<String> UTF8_COMPARATOR = new Comparator<String>() { 255 @Override 256 public int compare(String left, String right) { 257 byte[] a = left.getBytes(StandardCharsets.UTF_8); 258 byte[] b = right.getBytes(StandardCharsets.UTF_8); 259 int common = Math.min(a.length, b.length); 260 for (int index = 0; index < common; index++) { 261 int difference = (a[index] & 0xff) - (b[index] & 0xff); 262 if (difference != 0) { 263 return difference; 264 } 265 } 266 return a.length - b.length; 267 } 268 }; 269 270 private static final class Parser { 271 private final String input; 272 private int offset; 273 274 private Parser(String input) { 275 this.input = input; 276 } 277 278 private Object parse() { 279 skipWhitespace(); 280 Object value = value(); 281 skipWhitespace(); 282 if (offset != input.length()) { 283 fail("Unexpected trailing content"); 284 } 285 return value; 286 } 287 288 private Object value() { 289 if (offset >= input.length()) { 290 fail("Unexpected end of JSON"); 291 } 292 char current = input.charAt(offset); 293 if (current == '{') return object(); 294 if (current == '[') return array(); 295 if (current == '"') return string(); 296 if (current == 't') return literal("true", Boolean.TRUE); 297 if (current == 'f') return literal("false", Boolean.FALSE); 298 if (current == 'n') return literal("null", null); 299 if (current == '-' || (current >= '0' && current <= '9')) return integer(); 300 fail("Unexpected character '" + current + "'"); 301 return null; 302 } 303 304 private SlpcObject object() { 305 offset++; 306 skipWhitespace(); 307 SlpcObject.Builder builder = SlpcObject.builder(); 308 java.util.HashSet<String> names = new java.util.HashSet<String>(); 309 if (take('}')) return builder.build(); 310 while (true) { 311 if (peek() != '"') fail("Object member name must be a string"); 312 String name = string(); 313 if (!names.add(name)) fail("Duplicate object member '" + name + "'"); 314 skipWhitespace(); 315 require(':'); 316 skipWhitespace(); 317 builder.put(name, value()); 318 skipWhitespace(); 319 if (take('}')) return builder.build(); 320 require(','); 321 skipWhitespace(); 322 } 323 } 324 325 private List<Object> array() { 326 offset++; 327 skipWhitespace(); 328 ArrayList<Object> result = new ArrayList<Object>(); 329 if (take(']')) return Collections.unmodifiableList(result); 330 while (true) { 331 result.add(value()); 332 skipWhitespace(); 333 if (take(']')) return Collections.unmodifiableList(result); 334 require(','); 335 skipWhitespace(); 336 } 337 } 338 339 private String string() { 340 require('"'); 341 StringBuilder result = new StringBuilder(); 342 while (offset < input.length()) { 343 char current = input.charAt(offset++); 344 if (current == '"') { 345 String value = result.toString(); 346 rejectUnpairedSurrogates(value); 347 return value; 348 } 349 if (current == '\\') { 350 if (offset >= input.length()) fail("Incomplete escape sequence"); 351 char escaped = input.charAt(offset++); 352 switch (escaped) { 353 case '"': result.append('"'); break; 354 case '\\': result.append('\\'); break; 355 case '/': result.append('/'); break; 356 case 'b': result.append('\b'); break; 357 case 'f': result.append('\f'); break; 358 case 'n': result.append('\n'); break; 359 case 'r': result.append('\r'); break; 360 case 't': result.append('\t'); break; 361 case 'u': result.append(unicodeEscape()); break; 362 default: fail("Invalid escape sequence \\" + escaped); 363 } 364 } else { 365 if (current < 0x20) fail("Unescaped control character in string"); 366 result.append(current); 367 } 368 } 369 fail("Unterminated string"); 370 return null; 371 } 372 373 private char unicodeEscape() { 374 if (offset + 4 > input.length()) fail("Incomplete unicode escape"); 375 int value = 0; 376 for (int count = 0; count < 4; count++) { 377 char digit = input.charAt(offset++); 378 int hex = Character.digit(digit, 16); 379 if (hex < 0) fail("Invalid unicode escape"); 380 value = (value << 4) | hex; 381 } 382 return (char) value; 383 } 384 385 private BigInteger integer() { 386 int start = offset; 387 if (take('-') && offset >= input.length()) fail("Incomplete number"); 388 if (take('0')) { 389 if (offset < input.length() && isAsciiDigit(input.charAt(offset))) { 390 fail("Leading zero in number"); 391 } 392 } else { 393 if (offset >= input.length() || input.charAt(offset) < '1' || input.charAt(offset) > '9') { 394 fail("Invalid number"); 395 } 396 while (offset < input.length() && isAsciiDigit(input.charAt(offset))) offset++; 397 } 398 if (offset < input.length() && (input.charAt(offset) == '.' || input.charAt(offset) == 'e' 399 || input.charAt(offset) == 'E')) { 400 fail("SLPC JSON numbers must be integers"); 401 } 402 return new BigInteger(input.substring(start, offset)); 403 } 404 405 private Object literal(String text, Object value) { 406 if (!input.regionMatches(offset, text, 0, text.length())) fail("Invalid literal"); 407 offset += text.length(); 408 return value; 409 } 410 411 private void skipWhitespace() { 412 while (offset < input.length()) { 413 char current = input.charAt(offset); 414 if (current != ' ' && current != '\n' && current != '\r' && current != '\t') return; 415 offset++; 416 } 417 } 418 419 private char peek() { 420 return offset < input.length() ? input.charAt(offset) : '\0'; 421 } 422 423 private boolean take(char expected) { 424 if (offset < input.length() && input.charAt(offset) == expected) { 425 offset++; 426 return true; 427 } 428 return false; 429 } 430 431 private void require(char expected) { 432 if (!take(expected)) fail("Expected '" + expected + "'"); 433 } 434 435 private void fail(String message) { 436 throw new SlpcJsonException(message + " at character " + offset); 437 } 438 439 private boolean isAsciiDigit(char value) { 440 return value >= '0' && value <= '9'; 441 } 442 } 443}