package io.gitlab.jfronny.commons.serialize.json; import io.gitlab.jfronny.commons.data.LazilyParsedNumber; import io.gitlab.jfronny.commons.serialize.StringEscapeUtil; import io.gitlab.jfronny.commons.serialize.json.impl.JsonScope; import io.gitlab.jfronny.commons.serialize.MalformedDataException; import io.gitlab.jfronny.commons.serialize.SerializeReader; import io.gitlab.jfronny.commons.serialize.Token; import java.io.Closeable; import java.io.EOFException; import java.io.IOException; import java.io.Reader; import java.util.Arrays; import java.util.Objects; public class JsonReader extends SerializeReader implements Closeable { private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10; private static final int PEEKED_NONE = 0; private static final int PEEKED_BEGIN_OBJECT = 1; private static final int PEEKED_END_OBJECT = 2; private static final int PEEKED_BEGIN_ARRAY = 3; private static final int PEEKED_END_ARRAY = 4; private static final int PEEKED_TRUE = 5; private static final int PEEKED_FALSE = 6; private static final int PEEKED_NULL = 7; private static final int PEEKED_SINGLE_QUOTED = 8; private static final int PEEKED_DOUBLE_QUOTED = 9; private static final int PEEKED_UNQUOTED = 10; /** When this is returned, the string value is stored in peekedString. */ private static final int PEEKED_BUFFERED = 11; private static final int PEEKED_SINGLE_QUOTED_NAME = 12; private static final int PEEKED_DOUBLE_QUOTED_NAME = 13; private static final int PEEKED_UNQUOTED_NAME = 14; /** When this is returned, the integer value is stored in peekedLong. */ private static final int PEEKED_LONG = 15; private static final int PEEKED_NUMBER = 16; private static final int PEEKED_EOF = 17; /* State machine when parsing numbers */ private static final int NUMBER_CHAR_NONE = 0; private static final int NUMBER_CHAR_SIGN = 1; private static final int NUMBER_CHAR_DIGIT = 2; private static final int NUMBER_CHAR_DECIMAL = 3; private static final int NUMBER_CHAR_FRACTION_DIGIT = 4; private static final int NUMBER_CHAR_EXP_E = 5; private static final int NUMBER_CHAR_EXP_SIGN = 6; private static final int NUMBER_CHAR_EXP_DIGIT = 7; /** The input JSON. */ private final Reader in; static final int BUFFER_SIZE = 1024; /** * Use a manual buffer to easily read and unread upcoming characters, and also so we can create * strings without an intermediate StringBuilder. We decode literals directly out of this buffer, * so it must be at least as long as the longest token that can be reported as a number. */ private final char[] buffer = new char[BUFFER_SIZE]; private int pos = 0; private int limit = 0; private int lineNumber = 0; private int lineStart = 0; int peeked = PEEKED_NONE; /** * A peeked value that was composed entirely of digits with an optional leading dash. Positive * values may not have a leading 0. */ private long peekedLong; /** * The number of characters in a peeked number literal. Increment 'pos' by this after reading a * number. */ private int peekedNumberLength; /** * A peeked string that should be parsed on the next double, long or string. This is populated * before a numeric value is parsed and used if that parsing fails. */ private String peekedString; /* * The nesting stack. Using a manual array rather than an ArrayList saves 20%. */ private int[] stack = new int[32]; private int stackSize = 0; { stack[stackSize++] = JsonScope.EMPTY_DOCUMENT; } /* * The path members. It corresponds directly to stack: At indices where the * stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT), * pathNames contains the name at this scope. Where it contains an array * (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in * that array. Otherwise the value is undefined, and we take advantage of that * by incrementing pathIndices when doing so isn't useful. */ private String[] pathNames = new String[32]; private int[] pathIndices = new int[32]; private boolean wroteName = false; private Heuristics heuristics = Heuristics.DEFAULT; /** Creates a new instance that reads a JSON-encoded stream from {@code in}. */ public JsonReader(Reader in) { this.in = Objects.requireNonNull(in, "in == null"); } public JsonReader setHeuristics(Heuristics heuristics) { this.heuristics = Objects.requireNonNull(heuristics); return this; } public Heuristics getHeuristics() { return heuristics; } @Override public JsonReader beginArray() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_BEGIN_ARRAY) { wroteName = false; push(JsonScope.EMPTY_ARRAY); pathIndices[stackSize - 1] = 0; peeked = PEEKED_NONE; return this; } else { throw unexpectedTokenError("BEGIN_ARRAY"); } } @Override public JsonReader endArray() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_END_ARRAY) { wroteName = false; stackSize--; pathIndices[stackSize - 1]++; peeked = PEEKED_NONE; return this; } else { throw unexpectedTokenError("END_ARRAY"); } } @Override public JsonReader beginObject() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_BEGIN_OBJECT) { wroteName = false; push(JsonScope.EMPTY_OBJECT); peeked = PEEKED_NONE; return this; } else { throw unexpectedTokenError("BEGIN_OBJECT"); } } @Override public JsonReader endObject() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_END_OBJECT) { wroteName = false; stackSize--; pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! pathIndices[stackSize - 1]++; peeked = PEEKED_NONE; return this; } else { throw unexpectedTokenError("END_OBJECT"); } } @Override public boolean hasNext() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY && p != PEEKED_EOF; } @Override public Token peek() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } return switch (p) { case PEEKED_BEGIN_OBJECT -> Token.BEGIN_OBJECT; case PEEKED_END_OBJECT -> Token.END_OBJECT; case PEEKED_BEGIN_ARRAY -> Token.BEGIN_ARRAY; case PEEKED_END_ARRAY -> Token.END_ARRAY; case PEEKED_SINGLE_QUOTED_NAME, PEEKED_DOUBLE_QUOTED_NAME, PEEKED_UNQUOTED_NAME -> Token.NAME; case PEEKED_TRUE, PEEKED_FALSE -> Token.BOOLEAN; case PEEKED_NULL -> Token.NULL; case PEEKED_SINGLE_QUOTED, PEEKED_DOUBLE_QUOTED, PEEKED_UNQUOTED, PEEKED_BUFFERED -> Token.STRING; case PEEKED_LONG, PEEKED_NUMBER -> Token.NUMBER; case PEEKED_EOF -> Token.END_DOCUMENT; default -> throw new AssertionError(); }; } @SuppressWarnings("fallthrough") int doPeek() throws IOException { int peekStack = stack[stackSize - 1]; if (peekStack == JsonScope.EMPTY_ARRAY) { stack[stackSize - 1] = JsonScope.NONEMPTY_ARRAY; } else if (peekStack == JsonScope.NONEMPTY_ARRAY) { // Look for a comma before the next element. int c = nextNonWhitespace(true); switch (c) { case ']': return peeked = PEEKED_END_ARRAY; case ';': checkLenient(); // fall-through case ',': break; default: throw syntaxError("Unterminated array at " + (char)c); } } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) { stack[stackSize - 1] = JsonScope.DANGLING_NAME; // Look for a comma before the next element. if (peekStack == JsonScope.NONEMPTY_OBJECT) { int c = nextNonWhitespace(true); switch (c) { case '}': return peeked = PEEKED_END_OBJECT; case ';': checkLenient(); // fall-through case ',': break; default: throw syntaxError("Unterminated object at " + (char)c); } } int c = nextNonWhitespace(true); switch (c) { case '"': return peeked = PEEKED_DOUBLE_QUOTED_NAME; case '\'': checkLenient(); return peeked = PEEKED_SINGLE_QUOTED_NAME; case '}': if (peekStack != JsonScope.NONEMPTY_OBJECT) { return peeked = PEEKED_END_OBJECT; } else { throw syntaxError("Expected name"); } default: checkLenient(); pos--; // Don't consume the first character in an unquoted string. if (isLiteral((char) c)) { return peeked = PEEKED_UNQUOTED_NAME; } else { throw syntaxError("Expected name"); } } } else if (peekStack == JsonScope.DANGLING_NAME) { stack[stackSize - 1] = JsonScope.NONEMPTY_OBJECT; // Look for a colon before the value. int c = nextNonWhitespace(true); switch (c) { case ':': break; case '=': checkLenient(); if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') { pos++; } break; default: throw syntaxError("Expected ':'"); } } else if (peekStack == JsonScope.EMPTY_DOCUMENT) { if (lenient) { consumeNonExecutePrefix(); } stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT; } else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) { int c = nextNonWhitespace(false); if (c == -1) { return peeked = PEEKED_EOF; } else { checkLenient(); pos--; } } else if (peekStack == JsonScope.CLOSED) { throw new IllegalStateException("JsonReader is closed"); } if (checkNextNonWhitespace(peekStack)) return peeked; int result = peekKeyword(); if (result != PEEKED_NONE) { return result; } result = peekNumber(); if (result != PEEKED_NONE) { return result; } if (!isLiteral(buffer[pos])) { throw syntaxError("Expected value"); } checkLenient(); return peeked = PEEKED_UNQUOTED; } private boolean checkNextNonWhitespace(int peekStack) throws IOException { int c = nextNonWhitespace(true); switch (c) { case ']': if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) { peeked = PEEKED_END_ARRAY; return true; } throw syntaxError("Unexpected value"); case ';': case ',': // In lenient mode, a 0-length literal in an array should be skipped. if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) { checkLenient(); //pos--; //peeked = PEEKED_NULL; return checkNextNonWhitespace(peekStack); } else { throw syntaxError("Unexpected value"); } case '\'': checkLenient(); peeked = PEEKED_SINGLE_QUOTED; return true; case '"': peeked = PEEKED_DOUBLE_QUOTED; return true; case '[': peeked = PEEKED_BEGIN_ARRAY; return true; case '{': peeked = PEEKED_BEGIN_OBJECT; return true; default: pos--; // Don't consume the first character in a literal value. } return false; } private int peekKeyword() throws IOException { // Figure out which keyword we're matching against by its first character. char c = buffer[pos]; String keyword; String keywordUpper; int peeking; // Look at the first letter to determine what keyword we are trying to match. if (c == 't' || c == 'T') { keyword = "true"; keywordUpper = "TRUE"; peeking = PEEKED_TRUE; } else if (c == 'f' || c == 'F') { keyword = "false"; keywordUpper = "FALSE"; peeking = PEEKED_FALSE; } else if (c == 'n' || c == 'N') { keyword = "null"; keywordUpper = "NULL"; peeking = PEEKED_NULL; } else { return PEEKED_NONE; } // Uppercased keywords are not allowed in STRICT mode boolean allowsUpperCased = lenient; // Confirm that chars [0..length) match the keyword. int length = keyword.length(); for (int i = 0; i < length; i++) { if (pos + i >= limit && !fillBuffer(i + 1)) { return PEEKED_NONE; } c = buffer[pos + i]; boolean matched = c == keyword.charAt(i) || (allowsUpperCased && c == keywordUpper.charAt(i)); if (!matched) { return PEEKED_NONE; } } if ((pos + length < limit || fillBuffer(length + 1)) && isLiteral(buffer[pos + length])) { return PEEKED_NONE; // Don't match trues, falsey or nullsoft! } // We've found the keyword followed either by EOF or by a non-literal character. pos += length; return peeked = peeking; } private int peekNumber() throws IOException { // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access. char[] buffer = this.buffer; int p = pos; int l = limit; long value = 0; // Negative to accommodate Long.MIN_VALUE more easily. boolean negative = false; boolean fitsInLong = true; int last = NUMBER_CHAR_NONE; int i = 0; charactersOfNumber: for (; true; i++) { if (p + i == l) { if (i == buffer.length) { // Though this looks like a well-formed number, it's too long to continue reading. Give up // and let the application handle this as an unquoted literal. return PEEKED_NONE; } if (!fillBuffer(i + 1)) { break; } p = pos; l = limit; } char c = buffer[p + i]; switch (c) { case '-': if (last == NUMBER_CHAR_NONE) { negative = true; last = NUMBER_CHAR_SIGN; continue; } else if (last == NUMBER_CHAR_EXP_E) { last = NUMBER_CHAR_EXP_SIGN; continue; } return PEEKED_NONE; case '+': if (last == NUMBER_CHAR_EXP_E) { last = NUMBER_CHAR_EXP_SIGN; continue; } return PEEKED_NONE; case 'e': case 'E': if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) { last = NUMBER_CHAR_EXP_E; continue; } return PEEKED_NONE; case '.': if (last == NUMBER_CHAR_DIGIT) { last = NUMBER_CHAR_DECIMAL; continue; } return PEEKED_NONE; default: if (c < '0' || c > '9') { if (!isLiteral(c)) { break charactersOfNumber; } return PEEKED_NONE; } if (last == NUMBER_CHAR_SIGN || last == NUMBER_CHAR_NONE) { value = -(c - '0'); last = NUMBER_CHAR_DIGIT; } else if (last == NUMBER_CHAR_DIGIT) { if (value == 0) { return PEEKED_NONE; // Leading '0' prefix is not allowed (since it could be octal). } long newValue = value * 10 - (c - '0'); fitsInLong &= value > MIN_INCOMPLETE_INTEGER || (value == MIN_INCOMPLETE_INTEGER && newValue < value); value = newValue; } else if (last == NUMBER_CHAR_DECIMAL) { last = NUMBER_CHAR_FRACTION_DIGIT; } else if (last == NUMBER_CHAR_EXP_E || last == NUMBER_CHAR_EXP_SIGN) { last = NUMBER_CHAR_EXP_DIGIT; } } } // We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER. // Don't store -0 as long; user might want to read it as double -0.0 // Don't try to convert Long.MIN_VALUE to positive long; it would overflow MAX_VALUE if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative) && (value != 0 || !negative)) { peekedLong = negative ? value : -value; pos += i; return peeked = PEEKED_LONG; } else if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT || last == NUMBER_CHAR_EXP_DIGIT) { peekedNumberLength = i; return peeked = PEEKED_NUMBER; } else { return PEEKED_NONE; } } @SuppressWarnings("fallthrough") private boolean isLiteral(char c) throws IOException { switch (c) { case '/': case '\\': case ';': case '#': case '=': checkLenient(); // fall-through case '{': case '}': case '[': case ']': case ':': case ',': case ' ': case '\t': case '\f': case '\r': case '\n': return false; default: return true; } } @Override public String nextName() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } String result; switch (p) { case PEEKED_UNQUOTED_NAME -> result = nextUnquotedValue(); case PEEKED_SINGLE_QUOTED_NAME -> result = nextQuotedValue('\''); case PEEKED_DOUBLE_QUOTED_NAME -> result = nextQuotedValue('"'); default -> { // If we are in an array, allow reading an in inferred name once if (!wroteName) { if (stack[stackSize - 1] == JsonScope.EMPTY_ARRAY || stack[stackSize - 1] == JsonScope.NONEMPTY_ARRAY || stack[stackSize - 1] == JsonScope.EMPTY_DOCUMENT || stack[stackSize - 1] == JsonScope.NONEMPTY_DOCUMENT) { wroteName = true; return heuristics.guessArrayElementName(getPath()); } } throw unexpectedTokenError("a name"); } } wroteName = true; peeked = PEEKED_NONE; pathNames[stackSize - 1] = result; return result; } @Override public String nextString() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } String result; switch (p) { case PEEKED_UNQUOTED -> result = nextUnquotedValue(); case PEEKED_SINGLE_QUOTED -> result = nextQuotedValue('\''); case PEEKED_DOUBLE_QUOTED -> result = nextQuotedValue('"'); case PEEKED_BUFFERED -> { result = peekedString; peekedString = null; } case PEEKED_LONG -> result = Long.toString(peekedLong); case PEEKED_NUMBER -> { result = new String(buffer, pos, peekedNumberLength); pos += peekedNumberLength; } default -> throw unexpectedTokenError("a string"); } wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } @Override public boolean nextBoolean() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } return switch (p) { case PEEKED_TRUE -> { wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; yield true; } case PEEKED_FALSE -> { wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; yield false; } default -> throw unexpectedTokenError("a boolean"); }; } @Override public void nextNull() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_NULL) { wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; } else { throw unexpectedTokenError("null"); } } @Override public double nextDouble() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_LONG) { wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return (double) peekedLong; } if (p == PEEKED_NUMBER) { peekedString = new String(buffer, pos, peekedNumberLength); pos += peekedNumberLength; } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) { peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"'); } else if (p == PEEKED_UNQUOTED) { peekedString = nextUnquotedValue(); } else if (p != PEEKED_BUFFERED) { throw unexpectedTokenError("a double"); } peeked = PEEKED_BUFFERED; double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException. if (!serializeSpecialFloatingPointValues && (Double.isNaN(result) || Double.isInfinite(result))) { throw syntaxError("JSON forbids NaN and infinities: " + result); } wroteName = false; peekedString = null; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } @Override public long nextLong() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_LONG) { wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return peekedLong; } if (p == PEEKED_NUMBER) { peekedString = new String(buffer, pos, peekedNumberLength); pos += peekedNumberLength; } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED || p == PEEKED_UNQUOTED) { if (p == PEEKED_UNQUOTED) { peekedString = nextUnquotedValue(); } else { peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"'); } try { long result = Long.parseLong(peekedString); wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } catch (NumberFormatException ignored) { // Fall back to parse as a double below. } } else { throw unexpectedTokenError("a long"); } peeked = PEEKED_BUFFERED; double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException. long result = (long) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'long'. throw new NumberFormatException("Expected a long but was " + peekedString + locationString()); } peekedString = null; wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } @Override public int nextInt() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } int result; if (p == PEEKED_LONG) { result = (int) peekedLong; if (peekedLong != result) { // Make sure no precision was lost casting to 'int'. throw new NumberFormatException("Expected an int but was " + peekedLong + locationString()); } wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } if (p == PEEKED_NUMBER) { peekedString = new String(buffer, pos, peekedNumberLength); pos += peekedNumberLength; } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED || p == PEEKED_UNQUOTED) { if (p == PEEKED_UNQUOTED) { peekedString = nextUnquotedValue(); } else { peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"'); } try { result = Integer.parseInt(peekedString); wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } catch (NumberFormatException ignored) { // Fall back to parse as a double below. } } else { throw unexpectedTokenError("an int"); } peeked = PEEKED_BUFFERED; if (peekedString.startsWith("0x")) { result = Integer.decode(peekedString); } else { double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException. result = (int) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'int'. throw new NumberFormatException("Expected an int but was " + peekedString + locationString()); } } peekedString = null; wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } @Override public Number nextNumber() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_LONG) { wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return (double) peekedLong; } peekedString = switch (p) { case PEEKED_NUMBER -> { String res = new String(buffer, pos, peekedNumberLength); pos += peekedNumberLength; yield res; } case PEEKED_SINGLE_QUOTED -> nextQuotedValue('\''); case PEEKED_DOUBLE_QUOTED -> nextQuotedValue('"'); case PEEKED_UNQUOTED -> nextUnquotedValue(); case PEEKED_BUFFERED -> peekedString; default -> throw unexpectedTokenError("a double"); }; peeked = PEEKED_BUFFERED; LazilyParsedNumber result = new LazilyParsedNumber(peekedString); // don't catch this NumberFormatException. if (!serializeSpecialFloatingPointValues && (result.toString().equals("NaN") || result.toString().equals("Infinity") || result.toString().equals("-Infinity"))) { throw syntaxError("JSON forbids NaN and infinities: " + result); } peekedString = null; wroteName = false; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } @Override public void skipValue() throws IOException { int count = 0; do { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } switch (p) { case PEEKED_BEGIN_ARRAY -> { push(JsonScope.EMPTY_ARRAY); wroteName = false; count++; } case PEEKED_BEGIN_OBJECT -> { push(JsonScope.EMPTY_OBJECT); wroteName = false; count++; } case PEEKED_END_ARRAY -> { wroteName = false; stackSize--; count--; } case PEEKED_END_OBJECT -> { // Only update when object end is explicitly skipped, otherwise stack is not updated // anyways if (count == 0) { // Free the last path name so that it can be garbage collected pathNames[stackSize - 1] = null; } wroteName = false; stackSize--; count--; } case PEEKED_UNQUOTED -> { wroteName = false; skipUnquotedValue(); } case PEEKED_SINGLE_QUOTED -> { wroteName = false; skipQuotedValue('\''); } case PEEKED_DOUBLE_QUOTED -> { wroteName = false; skipQuotedValue('"'); } case PEEKED_UNQUOTED_NAME -> { skipUnquotedValue(); wroteName = true; // Only update when name is explicitly skipped, otherwise stack is not updated anyways if (count == 0) { pathNames[stackSize - 1] = ""; } } case PEEKED_SINGLE_QUOTED_NAME -> { skipQuotedValue('\''); wroteName = true; // Only update when name is explicitly skipped, otherwise stack is not updated anyways if (count == 0) { pathNames[stackSize - 1] = ""; } } case PEEKED_DOUBLE_QUOTED_NAME -> { skipQuotedValue('"'); wroteName = true; // Only update when name is explicitly skipped, otherwise stack is not updated anyways if (count == 0) { pathNames[stackSize - 1] = ""; } } case PEEKED_NUMBER -> { wroteName = false; pos += peekedNumberLength; } case PEEKED_EOF -> throw new IllegalStateException("Attempt to skip led outside the document"); // For all other tokens there is nothing to do; token has already been consumed from // underlying reader default -> wroteName = false; } peeked = PEEKED_NONE; } while (count > 0); pathIndices[stackSize - 1]++; if (count < 0) throw new IllegalStateException("Attempt to skip led outside its parent"); } private void push(int newTop) { if (stackSize == stack.length) { int newLength = stackSize * 2; stack = Arrays.copyOf(stack, newLength); pathIndices = Arrays.copyOf(pathIndices, newLength); pathNames = Arrays.copyOf(pathNames, newLength); } stack[stackSize++] = newTop; } /** * Returns true once {@code limit - pos >= minimum}. If the data is exhausted before that many * characters are available, this returns false. */ private boolean fillBuffer(int minimum) throws IOException { char[] buffer = this.buffer; lineStart -= pos; if (limit != pos) { limit -= pos; System.arraycopy(buffer, pos, buffer, 0, limit); } else { limit = 0; } pos = 0; int total; while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) { limit += total; // if this is the first read, consume an optional byte order mark (BOM) if it exists if (lineNumber == 0 && lineStart == 0 && limit > 0 && buffer[0] == '\ufeff') { pos++; lineStart++; minimum++; } if (limit >= minimum) { return true; } } return false; } /** * Returns the next character in the stream that is neither whitespace nor a part of a comment. * When this returns, the returned character is always at {@code buffer[pos-1]}; this means the * caller can always push back the returned character by decrementing {@code pos}. */ private int nextNonWhitespace(boolean throwOnEof) throws IOException { /* * This code uses ugly local variables 'p' and 'l' representing the 'pos' * and 'limit' fields respectively. Using locals rather than fields saves * a few field reads for each whitespace character in a pretty-printed * document, resulting in a 5% speedup. We need to flush 'p' to its field * before any (potentially indirect) call to fillBuffer() and reread both * 'p' and 'l' after any (potentially indirect) call to the same method. */ char[] buffer = this.buffer; int p = pos; int l = limit; while (true) { if (p == l) { pos = p; if (!fillBuffer(1)) { break; } p = pos; l = limit; } int c = buffer[p++]; if (c == '\n') { lineNumber++; lineStart = p; continue; } else if (c == ' ' || c == '\r' || c == '\t') { continue; } if (c == '/') { pos = p; if (p == l) { pos--; // push back '/' so it's still in the buffer when this method returns boolean charsLoaded = fillBuffer(2); pos++; // consume the '/' again if (!charsLoaded) { return c; } } checkLenient(); char peek = buffer[pos]; switch (peek) { case '*': // skip a /* c-style comment */ pos++; if (!skipTo("*/")) { throw syntaxError("Unterminated comment"); } p = pos + 2; l = limit; continue; case '/': // skip a // end-of-line comment pos++; skipToEndOfLine(); p = pos; l = limit; continue; default: return c; } } else if (c == '#') { pos = p; /* * Skip a # hash end-of-line comment. The JSON RFC doesn't * specify this behaviour, but it's required to parse * existing documents. See http://b/2571423. */ checkLenient(); skipToEndOfLine(); p = pos; l = limit; } else { pos = p; return c; } } if (throwOnEof) { throw new EOFException("End of input" + locationString()); } else { return -1; } } private void checkLenient() throws MalformedDataException { if (!lenient) { throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON"); } } /** * Advances the position until after the next newline character. If the line is terminated by * "\r\n", the '\n' must be consumed as whitespace by the caller. */ private void skipToEndOfLine() throws IOException { while (pos < limit || fillBuffer(1)) { char c = buffer[pos++]; if (c == '\n') { lineNumber++; lineStart = pos; break; } else if (c == '\r') { break; } } } /** * @param toFind a string to search for. Must not contain a newline. */ private boolean skipTo(String toFind) throws IOException { int length = toFind.length(); outer: for (; pos + length <= limit || fillBuffer(length); pos++) { if (buffer[pos] == '\n') { lineNumber++; lineStart = pos + 1; continue; } for (int c = 0; c < length; c++) { if (buffer[pos + c] != toFind.charAt(c)) { continue outer; } } return true; } return false; } /** * Returns the string up to but not including {@code quote}, unescaping any character escape * sequences encountered along the way. The opening quote should have already been read. This * consumes the closing quote, but does not include it in the returned string. * * @param quote either ' or ". */ private String nextQuotedValue(char quote) throws IOException { // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access. char[] buffer = this.buffer; StringBuilder builder = null; while (true) { int p = pos; int l = limit; /* the index of the first character not yet appended to the builder. */ int start = p; while (p < l) { int c = buffer[p++]; // In strict mode, throw an exception when meeting unescaped control characters (U+0000 // through U+001F) if (!lenient && c < 0x20) { throw syntaxError( "Unescaped control characters (\\u0000-\\u001F) are not allowed in strict mode"); } else if (c == quote) { pos = p; int len = p - start - 1; if (builder == null) { return new String(buffer, start, len); } else { builder.append(buffer, start, len); return builder.toString(); } } else if (c == '\\') { pos = p; int len = p - start - 1; if (builder == null) { int estimatedLength = (len + 1) * 2; builder = new StringBuilder(Math.max(estimatedLength, 16)); } builder.append(buffer, start, len); builder.append(readEscapeCharacter()); p = pos; l = limit; start = p; } else if (c == '\n') { lineNumber++; lineStart = p; } } if (builder == null) { int estimatedLength = (p - start) * 2; builder = new StringBuilder(Math.max(estimatedLength, 16)); } builder.append(buffer, start, p - start); pos = p; if (!fillBuffer(1)) { throw syntaxError("Unterminated string"); } } } /** Returns an unquoted value as a string. */ @SuppressWarnings("fallthrough") private String nextUnquotedValue() throws IOException { StringBuilder builder = null; int i = 0; findNonLiteralCharacter: while (true) { for (; pos + i < limit; i++) { switch (buffer[pos + i]) { case '/': case '\\': case ';': case '#': case '=': checkLenient(); // fall-through case '{': case '}': case '[': case ']': case ':': case ',': case ' ': case '\t': case '\f': case '\r': case '\n': break findNonLiteralCharacter; default: // skip character to be included in string value } } // Attempt to load the entire literal into the buffer at once. if (i < buffer.length) { if (fillBuffer(i + 1)) { continue; } else { break; } } // use a StringBuilder when the value is too long. This is too long to be a number! if (builder == null) { builder = new StringBuilder(Math.max(i, 16)); } builder.append(buffer, pos, i); pos += i; i = 0; if (!fillBuffer(1)) { break; } } String result = (null == builder) ? new String(buffer, pos, i) : builder.append(buffer, pos, i).toString(); pos += i; return result; } private void skipQuotedValue(char quote) throws IOException { // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access. char[] buffer = this.buffer; do { int p = pos; int l = limit; /* the index of the first character not yet appended to the builder. */ while (p < l) { int c = buffer[p++]; if (c == quote) { pos = p; return; } else if (c == '\\') { pos = p; char unused = readEscapeCharacter(); p = pos; l = limit; } else if (c == '\n') { lineNumber++; lineStart = p; } } pos = p; } while (fillBuffer(1)); throw syntaxError("Unterminated string"); } @SuppressWarnings("fallthrough") private void skipUnquotedValue() throws IOException { do { int i = 0; for (; pos + i < limit; i++) { switch (buffer[pos + i]) { case '/': case '\\': case ';': case '#': case '=': checkLenient(); // fall-through case '{': case '}': case '[': case ']': case ':': case ',': case ' ': case '\t': case '\f': case '\r': case '\n': pos += i; return; default: // skip the character } } pos += i; } while (fillBuffer(1)); } @Override protected String locationString() { int line = lineNumber + 1; int column = pos - lineStart + 1; String replacement = StringEscapeUtil.getReplacement(buffer[pos]); if (replacement == null) { replacement = String.valueOf(buffer[pos]); } String charInterjection = pos < buffer.length ? " (char '" + replacement + "')" : ""; return " at line " + line + " column " + column + charInterjection + " path " + getPath(); } private String getPath(boolean usePreviousPath) { StringBuilder result = new StringBuilder().append('$'); for (int i = 0; i < stackSize; i++) { int scope = stack[i]; switch (scope) { case JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY -> { int pathIndex = pathIndices[i]; // If index is last path element it points to next array element; have to decrement if (usePreviousPath && pathIndex > 0 && i == stackSize - 1) { pathIndex--; } result.append('[').append(pathIndex).append(']'); } case JsonScope.EMPTY_OBJECT, JsonScope.DANGLING_NAME, JsonScope.NONEMPTY_OBJECT -> { result.append('.'); if (pathNames[i] != null) { result.append(pathNames[i]); } } case JsonScope.NONEMPTY_DOCUMENT, JsonScope.EMPTY_DOCUMENT, JsonScope.CLOSED -> {} default -> throw new AssertionError("Unknown scope value: " + scope); } } return result.toString(); } @Override public String getPath() { return getPath(false); } @Override public String getPreviousPath() { return getPath(true); } /** * Unescapes the character identified by the character or characters that immediately follow a * backslash. The backslash '\' should have already been read. This supports both Unicode escapes * "u000A" and two-character escapes "\n". * * @throws MalformedDataException if the escape sequence is malformed */ @SuppressWarnings("fallthrough") private char readEscapeCharacter() throws IOException { if (pos == limit && !fillBuffer(1)) { throw syntaxError("Unterminated escape sequence"); } char escaped = buffer[pos++]; switch (escaped) { case 'u': if (pos + 4 > limit && !fillBuffer(4)) { throw syntaxError("Unterminated escape sequence"); } // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16); int result = 0; for (int i = pos, end = i + 4; i < end; i++) { char c = buffer[i]; result <<= 4; if (c >= '0' && c <= '9') { result += (c - '0'); } else if (c >= 'a' && c <= 'f') { result += (c - 'a' + 10); } else if (c >= 'A' && c <= 'F') { result += (c - 'A' + 10); } else { throw syntaxError("Malformed Unicode escape \\u" + new String(buffer, pos, 4)); } } pos += 4; return (char) result; case 't': return '\t'; case 'b': return '\b'; case 'n': return '\n'; case 'r': return '\r'; case 'f': return '\f'; case '\n': if (!lenient) { throw syntaxError("Cannot escape a newline character in strict mode"); } lineNumber++; lineStart = pos; // fall-through case '\'': if (!lenient) { throw syntaxError("Invalid escaped character \"'\" in strict mode"); } case '"': case '\\': case '/': return escaped; default: // throw error when none of the above cases are matched throw syntaxError("Invalid escape sequence"); } } /** * Throws a new {@link MalformedDataException} with the given message and information about the * current location. */ private MalformedDataException syntaxError(String message) throws MalformedDataException { throw new MalformedDataException(message + locationString()); } private IllegalStateException unexpectedTokenError(String expected) throws IOException { return new IllegalStateException("Expected " + expected + " but was " + peek() + locationString()); } /** Consumes the non-execute prefix if it exists. */ private void consumeNonExecutePrefix() throws IOException { // fast-forward through the leading whitespace int unused = nextNonWhitespace(true); pos--; if (pos + 5 > limit && !fillBuffer(5)) { return; } int p = pos; char[] buf = buffer; if (buf[p] != ')' || buf[p + 1] != ']' || buf[p + 2] != '}' || buf[p + 3] != '\'' || buf[p + 4] != '\n') { return; // not a security token! } // we consumed a security token! pos += 5; } public interface Heuristics { String guessArrayElementName(String path); Heuristics DEFAULT = (path) -> "item"; } @Override public void close() throws IOException { peeked = PEEKED_NONE; stack[0] = JsonScope.CLOSED; stackSize = 1; in.close(); } }