001package gudusoft.gsqlparser.lineage.v2.json;
002
003import gudusoft.gsqlparser.lineage.v2.SqllrException;
004
005import java.math.BigInteger;
006import java.nio.ByteBuffer;
007import java.nio.charset.CharacterCodingException;
008import java.nio.charset.CodingErrorAction;
009import java.nio.charset.StandardCharsets;
010import java.util.ArrayDeque;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.Collections;
014import java.util.Comparator;
015import java.util.Deque;
016import java.util.Iterator;
017import java.util.LinkedHashMap;
018import java.util.List;
019import java.util.Map;
020
021/**
022 * {@code canonical-json-v1} of SQL-LR ({@code lineage-spec/algorithms/canonical-json-v1.md}): the strict
023 * reader of Section 3, the RFC 8785 writer restricted to integers of Section 4, and the array ordering of
024 * the {@code DOCUMENT} mode (Sections 5 and 6).
025 *
026 * <p>A JSON value is one of these Java objects: {@code Map<String, Object>} (members in input order),
027 * {@code List<Object>}, {@code String}, {@code Long}, {@code Boolean}, or {@code null} for JSON null. The
028 * reader returns exactly these types; the writer also accepts {@code Integer}, {@code Short}, {@code Byte}
029 * and {@code BigInteger} within the integer range.</p>
030 *
031 * <p>The reference implementation is {@code lineage-spec/algorithms/reference/sqllr_ids.py}
032 * ({@code parse_json_strict}, {@code jcs_bytes}, {@code canonicalize_document}). Where the two can report
033 * different codes for one input, this class reports the code the reference reports: the parse runs left to
034 * right, a duplicate member name is detected when its object closes, and an unpaired surrogate is reported
035 * only after the whole text parsed.</p>
036 *
037 * <p>Every traversal uses an explicit stack, so nesting is limited by {@link #MAX_DEPTH} alone, not by the
038 * thread's stack size. A value nested deeper is {@code SCHEMA_INVALID_JSON} for the reader, the writer and
039 * the canonicalizer alike, so the writer never produces bytes the reader rejects. (The reference raises an
040 * uncaught {@code RecursionError} from about 500 levels on; no fixture comes near either limit.)</p>
041 *
042 * @since 4.2.17
043 */
044public final class CanonicalJson {
045
046    /** The two modes of Section 2, and the {@code DOCUMENT} mode that rejects unregistered arrays. */
047    public enum Mode {
048        /** Every array keeps its order. */
049        JCS,
050        /** Arrays are ordered as registered; an unregistered array keeps its order. */
051        DOCUMENT,
052        /** As {@link #DOCUMENT}, and an unregistered array outside open content is {@code SCHEMA_VIOLATION}. */
053        DOCUMENT_STRICT
054    }
055
056    /** 2^53 - 1: the largest integer the format admits (Section 3, rule 7). */
057    public static final long MAX_SAFE_INTEGER = 9007199254740991L;
058
059    /**
060     * The most objects and arrays that may enclose one another; the outermost container is level 1. A
061     * deeper value is {@code SCHEMA_INVALID_JSON}.
062     */
063    public static final int MAX_DEPTH = 1000;
064
065    public static final String SCHEMA_INVALID_JSON = "SCHEMA_INVALID_JSON";
066    public static final String SCHEMA_DUPLICATE_KEY = "SCHEMA_DUPLICATE_KEY";
067    public static final String SCHEMA_NON_INTEGER_NUMBER = "SCHEMA_NON_INTEGER_NUMBER";
068    public static final String SCHEMA_VIOLATION = "SCHEMA_VIOLATION";
069    public static final String SEMANTIC_ARRAY_KIND_VIOLATION = "SEMANTIC_ARRAY_KIND_VIOLATION";
070
071    private static final BigInteger MAX_BIG = BigInteger.valueOf(MAX_SAFE_INTEGER);
072
073    private CanonicalJson() {
074    }
075
076    // ---------------------------------------------------------------------------------------------
077    // Reading (Section 3)
078    // ---------------------------------------------------------------------------------------------
079
080    /**
081     * Parses JSON bytes under the eight input rules of Section 3.
082     *
083     * @throws SqllrException {@code SCHEMA_INVALID_JSON}, {@code SCHEMA_DUPLICATE_KEY} or
084     *                        {@code SCHEMA_NON_INTEGER_NUMBER}
085     */
086    public static Object parse(byte[] data) {
087        if (data.length >= 3 && (data[0] & 0xff) == 0xef && (data[1] & 0xff) == 0xbb && (data[2] & 0xff) == 0xbf) {
088            throw new SqllrException(SCHEMA_INVALID_JSON, "a byte order mark is not allowed");
089        }
090        String text;
091        try {
092            text = StandardCharsets.UTF_8.newDecoder()
093                    .onMalformedInput(CodingErrorAction.REPORT)
094                    .onUnmappableCharacter(CodingErrorAction.REPORT)
095                    .decode(ByteBuffer.wrap(data)).toString();
096        } catch (CharacterCodingException e) {
097            throw new SqllrException(SCHEMA_INVALID_JSON, "input is not well-formed UTF-8");
098        }
099        return new Parser(text).parseText();
100    }
101
102    /** The canonical bytes of JSON input bytes in the given mode. */
103    public static byte[] canonicalBytes(byte[] data, Mode mode) {
104        Object value = parse(data);
105        if (mode == Mode.JCS) {
106            return jcsBytes(value);
107        }
108        return jcsBytes(canonicalizeDocument(value, mode == Mode.DOCUMENT_STRICT, ArrayRegistry.standard(), null));
109    }
110
111    // ---------------------------------------------------------------------------------------------
112    // Writing (Section 4)
113    // ---------------------------------------------------------------------------------------------
114
115    /** The RFC 8785 bytes of a value, every array in its current order ({@code JCS} mode). */
116    public static byte[] jcsBytes(Object value) {
117        return jcsText(value).getBytes(StandardCharsets.UTF_8);
118    }
119
120    /**
121     * The RFC 8785 text of a value, every array in its current order. Its UTF-8 encoding is
122     * {@link #jcsBytes(Object)}.
123     */
124    public static String jcsText(Object value) {
125        StringBuilder out = new StringBuilder();
126        if (!isContainer(value)) {
127            writeScalar(value, out);
128            return out.toString();
129        }
130        Deque<WriteFrame> stack = new ArrayDeque<WriteFrame>();
131        stack.push(WriteFrame.open(value, out, 1));
132        while (!stack.isEmpty()) {
133            WriteFrame frame = stack.peek();
134            if (!frame.hasNext()) {
135                out.append(frame.close);
136                stack.pop();
137                continue;
138            }
139            Object child = frame.next(out);
140            if (isContainer(child)) {
141                stack.push(WriteFrame.open(child, out, stack.size() + 1));
142            } else {
143                writeScalar(child, out);
144            }
145        }
146        return out.toString();
147    }
148
149    /** {@code jcsBytes(canonicalizeDocument(value, true))}: the canonical wire form of a lineage document. */
150    public static byte[] documentBytes(Object value) {
151        return jcsBytes(canonicalizeDocument(value, true));
152    }
153
154    private static boolean isContainer(Object value) {
155        return value instanceof Map<?, ?> || value instanceof List<?>;
156    }
157
158    /** One object or array being written: its members in JCS order, or its elements in order. */
159    private static final class WriteFrame {
160        final char close;
161        private final Map<?, ?> map;
162        private final List<String> names;
163        private final Iterator<?> elements;
164        private int index;
165
166        private WriteFrame(char close, Map<?, ?> map, List<String> names, Iterator<?> elements) {
167            this.close = close;
168            this.map = map;
169            this.names = names;
170            this.elements = elements;
171        }
172
173        static WriteFrame open(Object value, StringBuilder out, int level) {
174            if (level > MAX_DEPTH) {
175                throw new SqllrException(SCHEMA_INVALID_JSON, "nesting deeper than " + MAX_DEPTH);
176            }
177            if (value instanceof Map<?, ?>) {
178                Map<?, ?> map = (Map<?, ?>) value;
179                List<String> names = new ArrayList<String>(map.size());
180                for (Object name : map.keySet()) {
181                    if (!(name instanceof String)) {
182                        throw new IllegalArgumentException("JSON member name is not a String: " + name);
183                    }
184                    names.add((String) name);
185                }
186                // String.compareTo compares UTF-16 code units, unsigned: RFC 8785, Section 3.2.3.
187                Collections.sort(names);
188                out.append('{');
189                return new WriteFrame('}', map, names, null);
190            }
191            out.append('[');
192            return new WriteFrame(']', null, null, ((List<?>) value).iterator());
193        }
194
195        boolean hasNext() {
196            return map != null ? index < names.size() : elements.hasNext();
197        }
198
199        /** Writes the separator (and the member name) before the next child and returns the child. */
200        Object next(StringBuilder out) {
201            if (index > 0) {
202                out.append(',');
203            }
204            if (map != null) {
205                String name = names.get(index++);
206                writeString(name, out);
207                out.append(':');
208                return map.get(name);
209            }
210            index++;
211            return elements.next();
212        }
213    }
214
215    private static void writeScalar(Object value, StringBuilder out) {
216        if (value == null) {
217            out.append("null");
218        } else if (value instanceof Boolean) {
219            out.append(((Boolean) value).booleanValue() ? "true" : "false");
220        } else if (value instanceof String) {
221            writeString((String) value, out);
222        } else if (value instanceof Number) {
223            out.append(integerValue((Number) value));
224        } else {
225            throw new IllegalArgumentException("not a JSON value: " + value.getClass().getName());
226        }
227    }
228
229    /** The value of an integer {@link Number} within the integer range of Section 3, rule 7. */
230    static long integerValue(Number number) {
231        if (number instanceof Long || number instanceof Integer || number instanceof Short || number instanceof Byte) {
232            long value = number.longValue();
233            if (value > MAX_SAFE_INTEGER || value < -MAX_SAFE_INTEGER) {
234                throw new SqllrException(SCHEMA_NON_INTEGER_NUMBER, "integer out of range: " + value);
235            }
236            return value;
237        }
238        if (number instanceof BigInteger) {
239            BigInteger big = (BigInteger) number;
240            if (big.abs().compareTo(MAX_BIG) > 0) {
241                throw new SqllrException(SCHEMA_NON_INTEGER_NUMBER, "integer out of range: " + big);
242            }
243            return big.longValue();
244        }
245        throw new SqllrException(SCHEMA_NON_INTEGER_NUMBER, String.valueOf(number));
246    }
247
248    private static void writeString(String value, StringBuilder out) {
249        if (firstUnpairedSurrogate(value) >= 0) {
250            throw new SqllrException(SCHEMA_INVALID_JSON, "unpaired surrogate in a string");
251        }
252        out.append('"');
253        for (int i = 0; i < value.length(); i++) {
254            char c = value.charAt(i);
255            switch (c) {
256                case '"': out.append("\\\""); break;
257                case '\\': out.append("\\\\"); break;
258                case '\b': out.append("\\b"); break;
259                case '\t': out.append("\\t"); break;
260                case '\n': out.append("\\n"); break;
261                case '\f': out.append("\\f"); break;
262                case '\r': out.append("\\r"); break;
263                default:
264                    if (c < 0x20) {
265                        out.append("\\u00").append(HEX[c >> 4]).append(HEX[c & 0xf]);
266                    } else {
267                        out.append(c);
268                    }
269            }
270        }
271        out.append('"');
272    }
273
274    private static final char[] HEX = "0123456789abcdef".toCharArray();
275
276    /** The index of the first unpaired surrogate code unit of {@code value}, or -1. */
277    static int firstUnpairedSurrogate(String value) {
278        for (int i = 0; i < value.length(); i++) {
279            char c = value.charAt(i);
280            if (Character.isHighSurrogate(c)) {
281                if (i + 1 < value.length() && Character.isLowSurrogate(value.charAt(i + 1))) {
282                    i++;
283                } else {
284                    return i;
285                }
286            } else if (Character.isLowSurrogate(c)) {
287                return i;
288            }
289        }
290        return -1;
291    }
292
293    // ---------------------------------------------------------------------------------------------
294    // Ordering (Sections 5 and 6)
295    // ---------------------------------------------------------------------------------------------
296
297    /** {@code canonicalizeDocument(value, strict, ArrayRegistry.standard(), null)}. */
298    public static Object canonicalizeDocument(Object value, boolean strict) {
299        return canonicalizeDocument(value, strict, ArrayRegistry.standard(), null);
300    }
301
302    /**
303     * A copy of a lineage document with every {@code SET} array sorted by its registered key; every
304     * {@code SEQUENCE} and {@code DISPLAY_ORDER} array, and every array in open content, keeps its order.
305     * Members keep their order (the writer orders them). Children are visited in input order and an array
306     * is checked after its elements, as the reference does, so the first rejection is the reference's.
307     *
308     * @param strict reject an unregistered array outside open content with {@code SCHEMA_VIOLATION}
309     *               (Section 5, rule 5); otherwise keep it in order
310     * @param notes  when not null and not strict, receives one line per unregistered array kept in order
311     * @throws SqllrException {@code SEMANTIC_ARRAY_KIND_VIOLATION} for a {@code SET} holding two equal elements
312     *                        or a {@code SEQUENCE} whose ordinals are not 0..n-1 (Section 5, rules 3 and 4);
313     *                        {@code SCHEMA_VIOLATION} as above
314     */
315    public static Object canonicalizeDocument(Object value, boolean strict, ArrayRegistry registry, List<String> notes) {
316        ArrayRegistry.Position root = registry.root();
317        if (!isContainer(value)) {
318            return value;
319        }
320        Deque<WalkFrame> stack = new ArrayDeque<WalkFrame>();
321        stack.push(new WalkFrame(value, root, "$", root != null && root.isOpenContent(), 1));
322        Object result = null;
323        while (!stack.isEmpty()) {
324            WalkFrame frame = stack.peek();
325            if (frame.hasNext()) {
326                WalkFrame child = frame.nextChild();
327                if (child != null) {
328                    stack.push(child);
329                }
330                continue;
331            }
332            stack.pop();
333            Object done = frame.finish(strict, notes);
334            if (stack.isEmpty()) {
335                result = done;
336            } else {
337                stack.peek().accept(done);
338            }
339        }
340        return result;
341    }
342
343    /** One object or array being canonicalized. */
344    private static final class WalkFrame {
345        private final ArrayRegistry.Position position;
346        private final String path;
347        private final boolean inOpen;
348        private final int level;
349        private final Iterator<? extends Map.Entry<?, ?>> members;
350        private final Map<String, Object> outMap;
351        private final Iterator<?> elements;
352        private final List<Object> outList;
353        private String pendingName;
354
355        WalkFrame(Object node, ArrayRegistry.Position position, String path, boolean inOpen, int level) {
356            if (level > MAX_DEPTH) {
357                throw new SqllrException(SCHEMA_INVALID_JSON, "nesting deeper than " + MAX_DEPTH);
358            }
359            this.position = position;
360            this.path = path;
361            this.inOpen = inOpen;
362            this.level = level;
363            if (node instanceof Map<?, ?>) {
364                this.members = ((Map<?, ?>) node).entrySet().iterator();
365                this.outMap = new LinkedHashMap<String, Object>();
366                this.elements = null;
367                this.outList = null;
368            } else {
369                this.members = null;
370                this.outMap = null;
371                this.elements = ((List<?>) node).iterator();
372                this.outList = new ArrayList<Object>(((List<?>) node).size());
373            }
374        }
375
376        boolean hasNext() {
377            return members != null ? members.hasNext() : elements.hasNext();
378        }
379
380        /** Takes the next child: a scalar is copied at once (null returned), a container gets a frame. */
381        WalkFrame nextChild() {
382            Object child;
383            ArrayRegistry.Position childPosition;
384            String childPath;
385            boolean childOpen;
386            if (members != null) {
387                Map.Entry<?, ?> entry = members.next();
388                String name = (String) entry.getKey();
389                child = entry.getValue();
390                childPosition = position == null ? null : position.member(name);
391                childPath = path + "." + name;
392                childOpen = inOpen || "extensions".equals(name);
393                pendingName = name;
394            } else {
395                child = elements.next();
396                childPosition = position == null ? null : position.element();
397                childPath = path + "[]";
398                childOpen = inOpen;
399            }
400            if (!isContainer(child)) {
401                accept(child);
402                return null;
403            }
404            childOpen = childOpen || (childPosition != null && childPosition.isOpenContent());
405            return new WalkFrame(child, childPosition, childPath, childOpen, level + 1);
406        }
407
408        void accept(Object value) {
409            if (outMap != null) {
410                outMap.put(pendingName, value);
411            } else {
412                outList.add(value);
413            }
414        }
415
416        Object finish(boolean strict, List<String> notes) {
417            if (outMap != null) {
418                return outMap;
419            }
420            ArrayRegistry.Registration registration = inOpen ? ArrayRegistry.Registration.OPEN_SEQUENCE
421                    : position == null ? null : position.registration();
422            if (registration == null) {
423                if (strict) {
424                    throw new SqllrException(SCHEMA_VIOLATION, "unregistered array " + path);
425                }
426                if (notes != null) {
427                    notes.add("unregistered array " + path + " kept in order");
428                }
429                return outList;
430            }
431            if (registration.getOrder() == ArrayRegistry.Order.SET) {
432                return sortSet(outList, registration.getSortKey(), path);
433            }
434            checkOrdinals(outList, path);
435            return outList;
436        }
437    }
438
439    private static List<Object> sortSet(List<Object> items, ArrayRegistry.SortKey key, String path) {
440        List<Keyed> keyed = new ArrayList<Keyed>(items.size());
441        for (Object item : items) {
442            keyed.add(new Keyed(item, key));
443        }
444        Collections.sort(keyed, KEYED_ORDER);
445        List<Object> out = new ArrayList<Object>(items.size());
446        for (int i = 0; i < keyed.size(); i++) {
447            if (i > 0 && Arrays.equals(keyed.get(i - 1).bytes, keyed.get(i).bytes)) {
448                throw new SqllrException(SEMANTIC_ARRAY_KIND_VIOLATION, path + " holds equal elements");
449            }
450            out.add(keyed.get(i).element);
451        }
452        return out;
453    }
454
455    /** Section 5, rule 3: when every element carries an integer ordinal, they are 0..n-1 in array order. */
456    private static void checkOrdinals(List<Object> items, String path) {
457        if (items.isEmpty()) {
458            return;
459        }
460        for (Object item : items) {
461            if (!(item instanceof Map<?, ?>) || !isInteger(((Map<?, ?>) item).get("ordinal"))) {
462                return;
463            }
464        }
465        for (int i = 0; i < items.size(); i++) {
466            long ordinal = integerValue((Number) ((Map<?, ?>) items.get(i)).get("ordinal"));
467            if (ordinal != i) {
468                throw new SqllrException(SEMANTIC_ARRAY_KIND_VIOLATION, path + " ordinals are not 0..n-1 in array order");
469            }
470        }
471    }
472
473    /** A deep copy of a JSON value in which every object and array is unmodifiable. */
474    public static Object immutableCopy(Object value) {
475        if (!isContainer(value)) {
476            return value;
477        }
478        Deque<CopyFrame> stack = new ArrayDeque<CopyFrame>();
479        stack.push(new CopyFrame(value, 1));
480        Object result = null;
481        while (!stack.isEmpty()) {
482            CopyFrame frame = stack.peek();
483            if (frame.hasNext()) {
484                Object child = frame.next();
485                if (isContainer(child)) {
486                    stack.push(new CopyFrame(child, stack.size() + 1));
487                } else {
488                    frame.accept(child);
489                }
490                continue;
491            }
492            stack.pop();
493            Object done = frame.finish();
494            if (stack.isEmpty()) {
495                result = done;
496            } else {
497                stack.peek().accept(done);
498            }
499        }
500        return result;
501    }
502
503    /** One object or array being copied. */
504    private static final class CopyFrame {
505        private final Iterator<? extends Map.Entry<?, ?>> members;
506        private final Map<String, Object> outMap;
507        private final Iterator<?> elements;
508        private final List<Object> outList;
509        private String pendingName;
510
511        CopyFrame(Object node, int level) {
512            if (level > MAX_DEPTH) {
513                throw new SqllrException(SCHEMA_INVALID_JSON, "nesting deeper than " + MAX_DEPTH);
514            }
515            if (node instanceof Map<?, ?>) {
516                members = ((Map<?, ?>) node).entrySet().iterator();
517                outMap = new LinkedHashMap<String, Object>();
518                elements = null;
519                outList = null;
520            } else {
521                members = null;
522                outMap = null;
523                elements = ((List<?>) node).iterator();
524                outList = new ArrayList<Object>(((List<?>) node).size());
525            }
526        }
527
528        boolean hasNext() {
529            return members != null ? members.hasNext() : elements.hasNext();
530        }
531
532        Object next() {
533            if (members != null) {
534                Map.Entry<?, ?> entry = members.next();
535                pendingName = (String) entry.getKey();
536                return entry.getValue();
537            }
538            return elements.next();
539        }
540
541        void accept(Object value) {
542            if (outMap != null) {
543                outMap.put(pendingName, value);
544            } else {
545                outList.add(value);
546            }
547        }
548
549        Object finish() {
550            return outMap != null ? Collections.unmodifiableMap(outMap) : Collections.unmodifiableList(outList);
551        }
552    }
553
554    /** True for a JSON integer (not a boolean). */
555    public static boolean isInteger(Object value) {
556        return value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte
557                || value instanceof BigInteger;
558    }
559
560    /** An element with its precomputed sort key and canonical bytes. */
561    private static final class Keyed {
562        final Object element;
563        final TypedKey[] key;
564        final byte[] bytes;
565
566        Keyed(Object element, ArrayRegistry.SortKey sortKey) {
567            this.element = element;
568            this.bytes = jcsBytes(element);
569            switch (sortKey.getKind()) {
570                case SELF:
571                    key = new TypedKey[] {new TypedKey(element, bytes)};
572                    break;
573                case REF:
574                    key = new TypedKey[] {new TypedKey(member(element, "ref"), null)};
575                    break;
576                case BYTES:
577                    key = new TypedKey[0];
578                    break;
579                default:
580                    List<String> names = sortKey.getMembers();
581                    key = new TypedKey[names.size()];
582                    for (int i = 0; i < key.length; i++) {
583                        key[i] = new TypedKey(member(element, names.get(i)), null);
584                    }
585            }
586        }
587
588        private static Object member(Object element, String name) {
589            return element instanceof Map<?, ?> ? ((Map<?, ?>) element).get(name) : null;
590        }
591    }
592
593    private static final Comparator<Keyed> KEYED_ORDER = new Comparator<Keyed>() {
594        @Override
595        public int compare(Keyed a, Keyed b) {
596            for (int i = 0; i < a.key.length; i++) {
597                int c = a.key[i].compareTo(b.key[i]);
598                if (c != 0) {
599                    return c;
600                }
601            }
602            return compareUnsigned(a.bytes, b.bytes);
603        }
604    };
605
606    /**
607     * The total order of Section 6.2 over JSON values: absent or {@code null} &lt; {@code false} &lt;
608     * {@code true} &lt; integers (numerically) &lt; strings (unsigned UTF-8 bytes) &lt; arrays and objects
609     * (their canonical bytes, unsigned).
610     */
611    public static int compareTyped(Object a, Object b) {
612        return new TypedKey(a, null).compareTo(new TypedKey(b, null));
613    }
614
615    /** Unsigned lexicographic order of byte arrays. */
616    public static int compareUnsigned(byte[] a, byte[] b) {
617        int common = Math.min(a.length, b.length);
618        for (int i = 0; i < common; i++) {
619            int c = (a[i] & 0xff) - (b[i] & 0xff);
620            if (c != 0) {
621                return c;
622            }
623        }
624        return a.length - b.length;
625    }
626
627    /** One component of a sort key, ranked as in Section 6.2. */
628    private static final class TypedKey implements Comparable<TypedKey> {
629        final int rank;
630        final long number;
631        final byte[] bytes;
632
633        TypedKey(Object value, byte[] canonical) {
634            if (value == null) {
635                rank = 0;
636                number = 0;
637                bytes = null;
638            } else if (value instanceof Boolean) {
639                rank = 1;
640                number = ((Boolean) value).booleanValue() ? 1 : 0;
641                bytes = null;
642            } else if (value instanceof Number) {
643                rank = 2;
644                number = integerValue((Number) value);
645                bytes = null;
646            } else if (value instanceof String) {
647                if (firstUnpairedSurrogate((String) value) >= 0) {
648                    throw new SqllrException(SCHEMA_INVALID_JSON, "unpaired surrogate in a string");
649                }
650                rank = 3;
651                number = 0;
652                bytes = ((String) value).getBytes(StandardCharsets.UTF_8);
653            } else {
654                rank = 4;
655                number = 0;
656                bytes = canonical != null ? canonical : jcsBytes(value);
657            }
658        }
659
660        @Override
661        public int compareTo(TypedKey other) {
662            if (rank != other.rank) {
663                return rank < other.rank ? -1 : 1;
664            }
665            if (rank == 1 || rank == 2) {
666                return number < other.number ? -1 : number == other.number ? 0 : 1;
667            }
668            if (rank == 0) {
669                return 0;
670            }
671            return compareUnsigned(bytes, other.bytes);
672        }
673    }
674
675    // ---------------------------------------------------------------------------------------------
676    // The parser
677    // ---------------------------------------------------------------------------------------------
678
679    /**
680     * An RFC 8259 parser that follows the decisions of CPython's {@code json} scanner as the reference
681     * configures it (strict strings, integers through {@code parse_int}, fractions and exponents rejected,
682     * duplicate names detected by the object hook when the object closes), with an explicit stack.
683     */
684    private static final class Parser {
685        private final String s;
686        private final int length;
687        private int pos;
688        private boolean unpairedSurrogate;
689
690        /** An object or array whose closing bracket has not been read yet. */
691        private static final class Open {
692            final List<String> names;
693            final List<Object> values;
694            String pendingName;
695
696            Open(boolean object) {
697                names = object ? new ArrayList<String>() : null;
698                values = new ArrayList<Object>();
699            }
700        }
701
702        Parser(String s) {
703            this.s = s;
704            this.length = s.length();
705        }
706
707        Object parseText() {
708            skipWhitespace();
709            Object value = value();
710            skipWhitespace();
711            if (pos != length) {
712                throw invalid("extra data");
713            }
714            if (unpairedSurrogate) {
715                throw new SqllrException(SCHEMA_INVALID_JSON, "unpaired surrogate in a string or member name");
716            }
717            return value;
718        }
719
720        /** One JSON value at {@code pos}, the text before it already skipped. */
721        private Object value() {
722            Deque<Open> stack = new ArrayDeque<Open>();
723            while (true) {
724                // Read the start of a value: a scalar completes it, a non-empty container opens a level.
725                Object completed;
726                if (pos >= length) {
727                    throw invalid("expecting value");
728                }
729                char c = s.charAt(pos);
730                if (c == '{' || c == '[') {
731                    if (stack.size() + 1 > MAX_DEPTH) {
732                        throw invalid("nesting deeper than " + MAX_DEPTH);
733                    }
734                    pos++;
735                    skipWhitespace();
736                    boolean object = c == '{';
737                    if (pos < length && s.charAt(pos) == (object ? '}' : ']')) {
738                        pos++;
739                        completed = object ? new LinkedHashMap<String, Object>() : new ArrayList<Object>();
740                    } else {
741                        Open open = new Open(object);
742                        if (object) {
743                            open.pendingName = memberName();
744                        }
745                        stack.push(open);
746                        continue;
747                    }
748                } else {
749                    completed = scalar(c);
750                }
751                // Attach the completed value to the open containers, closing those that end here.
752                while (true) {
753                    Open top = stack.peek();
754                    if (top == null) {
755                        return completed;
756                    }
757                    boolean object = top.names != null;
758                    if (object) {
759                        top.names.add(top.pendingName);
760                    }
761                    top.values.add(completed);
762                    skipWhitespace();
763                    if (pos < length && s.charAt(pos) == (object ? '}' : ']')) {
764                        pos++;
765                        stack.pop();
766                        completed = object ? closeObject(top) : top.values;
767                        continue;
768                    }
769                    if (pos >= length || s.charAt(pos) != ',') {
770                        throw invalid("expecting ',' delimiter");
771                    }
772                    pos++;
773                    skipWhitespace();
774                    if (object) {
775                        top.pendingName = memberName();
776                    }
777                    break;
778                }
779            }
780        }
781
782        /** {@code "name"}, whitespace, {@code :}, whitespace; the reader is at the opening quote. */
783        private String memberName() {
784            if (pos >= length || s.charAt(pos) != '"') {
785                throw invalid("expecting property name enclosed in double quotes");
786            }
787            pos++;
788            String name = string();
789            skipWhitespace();
790            if (pos >= length || s.charAt(pos) != ':') {
791                throw invalid("expecting ':' delimiter");
792            }
793            pos++;
794            skipWhitespace();
795            return name;
796        }
797
798        /** Rule 5 is checked when the object closes, as the reference's object_pairs_hook does. */
799        private Map<String, Object> closeObject(Open open) {
800            Map<String, Object> out = new LinkedHashMap<String, Object>(open.names.size() * 2);
801            for (int i = 0; i < open.names.size(); i++) {
802                if (out.containsKey(open.names.get(i))) {
803                    throw new SqllrException(SCHEMA_DUPLICATE_KEY, "duplicate member " + open.names.get(i));
804                }
805                out.put(open.names.get(i), open.values.get(i));
806            }
807            return out;
808        }
809
810        private Object scalar(char c) {
811            switch (c) {
812                case '"':
813                    pos++;
814                    return string();
815                case 'n':
816                    return literal("null", null);
817                case 't':
818                    return literal("true", Boolean.TRUE);
819                case 'f':
820                    return literal("false", Boolean.FALSE);
821                default:
822                    if (c == '-' || (c >= '0' && c <= '9')) {
823                        return number();
824                    }
825                    // NaN, Infinity and every other token are not JSON.
826                    throw invalid("expecting value");
827            }
828        }
829
830        private Object literal(String word, Object value) {
831            if (!s.startsWith(word, pos)) {
832                throw invalid("expecting value");
833            }
834            pos += word.length();
835            return value;
836        }
837
838        /** The string whose opening quote has been consumed. */
839        private String string() {
840            StringBuilder out = new StringBuilder();
841            while (true) {
842                if (pos >= length) {
843                    throw invalid("unterminated string");
844                }
845                char c = s.charAt(pos++);
846                if (c == '"') {
847                    break;
848                }
849                if (c == '\\') {
850                    if (pos >= length) {
851                        throw invalid("unterminated string");
852                    }
853                    char e = s.charAt(pos++);
854                    switch (e) {
855                        case '"': out.append('"'); break;
856                        case '\\': out.append('\\'); break;
857                        case '/': out.append('/'); break;
858                        case 'b': out.append('\b'); break;
859                        case 'f': out.append('\f'); break;
860                        case 'n': out.append('\n'); break;
861                        case 'r': out.append('\r'); break;
862                        case 't': out.append('\t'); break;
863                        case 'u': out.append(hex4()); break;
864                        default: throw invalid("invalid escape");
865                    }
866                } else if (c < 0x20) {
867                    throw invalid("invalid control character in a string");
868                } else {
869                    out.append(c);
870                }
871            }
872            String value = out.toString();
873            if (firstUnpairedSurrogate(value) >= 0) {
874                // Reported after the whole text parsed, as the reference checks values after json.loads.
875                unpairedSurrogate = true;
876            }
877            return value;
878        }
879
880        private char hex4() {
881            if (pos + 4 > length) {
882                throw invalid("invalid \\uXXXX escape");
883            }
884            int value = 0;
885            for (int i = 0; i < 4; i++) {
886                char c = s.charAt(pos++);
887                int digit;
888                if (c >= '0' && c <= '9') {
889                    digit = c - '0';
890                } else if (c >= 'a' && c <= 'f') {
891                    digit = c - 'a' + 10;
892                } else if (c >= 'A' && c <= 'F') {
893                    digit = c - 'A' + 10;
894                } else {
895                    throw invalid("invalid \\uXXXX escape");
896                }
897                value = (value << 4) | digit;
898            }
899            return (char) value;
900        }
901
902        private Long number() {
903            int start = pos;
904            if (s.charAt(pos) == '-') {
905                pos++;
906                if (pos >= length) {
907                    throw invalid("expecting value");
908                }
909            }
910            char c = s.charAt(pos);
911            if (c >= '1' && c <= '9') {
912                pos++;
913                while (pos < length && isDigit(s.charAt(pos))) {
914                    pos++;
915                }
916            } else if (c == '0') {
917                pos++;
918            } else {
919                throw invalid("expecting value");
920            }
921            boolean fraction = false;
922            if (pos + 1 < length && s.charAt(pos) == '.' && isDigit(s.charAt(pos + 1))) {
923                pos += 2;
924                while (pos < length && isDigit(s.charAt(pos))) {
925                    pos++;
926                }
927                fraction = true;
928            }
929            if (pos < length && (s.charAt(pos) == 'e' || s.charAt(pos) == 'E')) {
930                int exponentStart = pos;
931                pos++;
932                if (pos < length && (s.charAt(pos) == '-' || s.charAt(pos) == '+')) {
933                    pos++;
934                }
935                int digits = pos;
936                while (pos < length && isDigit(s.charAt(pos))) {
937                    pos++;
938                }
939                if (pos > digits) {
940                    fraction = true;
941                } else {
942                    pos = exponentStart;
943                }
944            }
945            String token = s.substring(start, pos);
946            if (fraction) {
947                throw new SqllrException(SCHEMA_NON_INTEGER_NUMBER, token);
948            }
949            if ("-0".equals(token)) {
950                throw new SqllrException(SCHEMA_NON_INTEGER_NUMBER, "-0 is not a canonical integer");
951            }
952            int digitCount = token.startsWith("-") ? token.length() - 1 : token.length();
953            if (digitCount > 16) {
954                throw new SqllrException(SCHEMA_NON_INTEGER_NUMBER, "integer out of range: " + token);
955            }
956            long value = Long.parseLong(token);
957            if (value > MAX_SAFE_INTEGER || value < -MAX_SAFE_INTEGER) {
958                throw new SqllrException(SCHEMA_NON_INTEGER_NUMBER, "integer out of range: " + token);
959            }
960            return Long.valueOf(value);
961        }
962
963        private static boolean isDigit(char c) {
964            return c >= '0' && c <= '9';
965        }
966
967        private void skipWhitespace() {
968            while (pos < length) {
969                char c = s.charAt(pos);
970                if (c != ' ' && c != '\t' && c != '\n' && c != '\r') {
971                    return;
972                }
973                pos++;
974            }
975        }
976
977        private SqllrException invalid(String message) {
978            return new SqllrException(SCHEMA_INVALID_JSON, message + " at character " + pos);
979        }
980    }
981}