| /* |
| * Copyright (C) 2010 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.airbnb.lottie.parser.moshi; |
| |
| import java.io.EOFException; |
| import java.io.IOException; |
| |
| import androidx.annotation.Nullable; |
| import okio.Buffer; |
| import okio.BufferedSource; |
| import okio.ByteString; |
| |
| final class JsonUtf8Reader extends JsonReader { |
| private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10; |
| |
| private static final ByteString SINGLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("'\\"); |
| private static final ByteString DOUBLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("\"\\"); |
| private static final ByteString UNQUOTED_STRING_TERMINALS |
| = ByteString.encodeUtf8("{}[]:, \n\t\r\f/\\;#="); |
| private static final ByteString LINEFEED_OR_CARRIAGE_RETURN = ByteString.encodeUtf8("\n\r"); |
| private static final ByteString CLOSING_BLOCK_COMMENT = ByteString.encodeUtf8("*/"); |
| |
| 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; |
| private static final int PEEKED_BUFFERED_NAME = 15; |
| /** When this is returned, the integer value is stored in peekedLong. */ |
| private static final int PEEKED_LONG = 16; |
| private static final int PEEKED_NUMBER = 17; |
| private static final int PEEKED_EOF = 18; |
| |
| /* 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 BufferedSource source; |
| private final Buffer buffer; |
| |
| private 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. |
| */ |
| 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 @Nullable |
| String peekedString; |
| |
| JsonUtf8Reader(BufferedSource source) { |
| if (source == null) { |
| throw new NullPointerException("source == null"); |
| } |
| this.source = source; |
| this.buffer = source.getBuffer(); |
| pushScope(JsonScope.EMPTY_DOCUMENT); |
| } |
| |
| |
| @Override public void beginArray() throws IOException { |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| if (p == PEEKED_BEGIN_ARRAY) { |
| pushScope(JsonScope.EMPTY_ARRAY); |
| pathIndices[stackSize - 1] = 0; |
| peeked = PEEKED_NONE; |
| } else { |
| throw new JsonDataException("Expected BEGIN_ARRAY but was " + peek() |
| + " at path " + getPath()); |
| } |
| } |
| |
| @Override public void endArray() throws IOException { |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| if (p == PEEKED_END_ARRAY) { |
| stackSize--; |
| pathIndices[stackSize - 1]++; |
| peeked = PEEKED_NONE; |
| } else { |
| throw new JsonDataException("Expected END_ARRAY but was " + peek() |
| + " at path " + getPath()); |
| } |
| } |
| |
| @Override public void beginObject() throws IOException { |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| if (p == PEEKED_BEGIN_OBJECT) { |
| pushScope(JsonScope.EMPTY_OBJECT); |
| peeked = PEEKED_NONE; |
| } else { |
| throw new JsonDataException("Expected BEGIN_OBJECT but was " + peek() |
| + " at path " + getPath()); |
| } |
| } |
| |
| @Override public void endObject() throws IOException { |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| if (p == PEEKED_END_OBJECT) { |
| stackSize--; |
| pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! |
| pathIndices[stackSize - 1]++; |
| peeked = PEEKED_NONE; |
| } else { |
| throw new JsonDataException("Expected END_OBJECT but was " + peek() |
| + " at path " + getPath()); |
| } |
| } |
| |
| @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(); |
| } |
| |
| switch (p) { |
| case PEEKED_BEGIN_OBJECT: |
| return Token.BEGIN_OBJECT; |
| case PEEKED_END_OBJECT: |
| return Token.END_OBJECT; |
| case PEEKED_BEGIN_ARRAY: |
| return Token.BEGIN_ARRAY; |
| case PEEKED_END_ARRAY: |
| return Token.END_ARRAY; |
| case PEEKED_SINGLE_QUOTED_NAME: |
| case PEEKED_DOUBLE_QUOTED_NAME: |
| case PEEKED_UNQUOTED_NAME: |
| case PEEKED_BUFFERED_NAME: |
| return Token.NAME; |
| case PEEKED_TRUE: |
| case PEEKED_FALSE: |
| return Token.BOOLEAN; |
| case PEEKED_NULL: |
| return Token.NULL; |
| case PEEKED_SINGLE_QUOTED: |
| case PEEKED_DOUBLE_QUOTED: |
| case PEEKED_UNQUOTED: |
| case PEEKED_BUFFERED: |
| return Token.STRING; |
| case PEEKED_LONG: |
| case PEEKED_NUMBER: |
| return Token.NUMBER; |
| case PEEKED_EOF: |
| return Token.END_DOCUMENT; |
| default: |
| throw new AssertionError(); |
| } |
| } |
| |
| private int doPeek() throws IOException { |
| int peekStack = scopes[stackSize - 1]; |
| if (peekStack == JsonScope.EMPTY_ARRAY) { |
| scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY; |
| } else if (peekStack == JsonScope.NONEMPTY_ARRAY) { |
| // Look for a comma before the next element. |
| int c = nextNonWhitespace(true); |
| buffer.readByte(); // consume ']' or ','. |
| switch (c) { |
| case ']': |
| return peeked = PEEKED_END_ARRAY; |
| case ';': |
| checkLenient(); // fall-through |
| case ',': |
| break; |
| default: |
| throw syntaxError("Unterminated array"); |
| } |
| } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) { |
| scopes[stackSize - 1] = JsonScope.DANGLING_NAME; |
| // Look for a comma before the next element. |
| if (peekStack == JsonScope.NONEMPTY_OBJECT) { |
| int c = nextNonWhitespace(true); |
| buffer.readByte(); // Consume '}' or ','. |
| switch (c) { |
| case '}': |
| return peeked = PEEKED_END_OBJECT; |
| case ';': |
| checkLenient(); // fall-through |
| case ',': |
| break; |
| default: |
| throw syntaxError("Unterminated object"); |
| } |
| } |
| int c = nextNonWhitespace(true); |
| switch (c) { |
| case '"': |
| buffer.readByte(); // consume the '\"'. |
| return peeked = PEEKED_DOUBLE_QUOTED_NAME; |
| case '\'': |
| buffer.readByte(); // consume the '\''. |
| checkLenient(); |
| return peeked = PEEKED_SINGLE_QUOTED_NAME; |
| case '}': |
| if (peekStack != JsonScope.NONEMPTY_OBJECT) { |
| buffer.readByte(); // consume the '}'. |
| return peeked = PEEKED_END_OBJECT; |
| } else { |
| throw syntaxError("Expected name"); |
| } |
| default: |
| checkLenient(); |
| if (isLiteral((char) c)) { |
| return peeked = PEEKED_UNQUOTED_NAME; |
| } else { |
| throw syntaxError("Expected name"); |
| } |
| } |
| } else if (peekStack == JsonScope.DANGLING_NAME) { |
| scopes[stackSize - 1] = JsonScope.NONEMPTY_OBJECT; |
| // Look for a colon before the value. |
| int c = nextNonWhitespace(true); |
| buffer.readByte(); // Consume ':'. |
| switch (c) { |
| case ':': |
| break; |
| case '=': |
| checkLenient(); |
| if (source.request(1) && buffer.getByte(0) == '>') { |
| buffer.readByte(); // Consume '>'. |
| } |
| break; |
| default: |
| throw syntaxError("Expected ':'"); |
| } |
| } else if (peekStack == JsonScope.EMPTY_DOCUMENT) { |
| scopes[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT; |
| } else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) { |
| int c = nextNonWhitespace(false); |
| if (c == -1) { |
| return peeked = PEEKED_EOF; |
| } else { |
| checkLenient(); |
| } |
| } else if (peekStack == JsonScope.CLOSED) { |
| throw new IllegalStateException("JsonReader is closed"); |
| } |
| |
| int c = nextNonWhitespace(true); |
| switch (c) { |
| case ']': |
| if (peekStack == JsonScope.EMPTY_ARRAY) { |
| buffer.readByte(); // Consume ']'. |
| return peeked = PEEKED_END_ARRAY; |
| } |
| // fall-through to handle ",]" |
| case ';': |
| case ',': |
| // In lenient mode, a 0-length literal in an array means 'null'. |
| if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) { |
| checkLenient(); |
| return peeked = PEEKED_NULL; |
| } else { |
| throw syntaxError("Unexpected value"); |
| } |
| case '\'': |
| checkLenient(); |
| buffer.readByte(); // Consume '\''. |
| return peeked = PEEKED_SINGLE_QUOTED; |
| case '"': |
| buffer.readByte(); // Consume '\"'. |
| return peeked = PEEKED_DOUBLE_QUOTED; |
| case '[': |
| buffer.readByte(); // Consume '['. |
| return peeked = PEEKED_BEGIN_ARRAY; |
| case '{': |
| buffer.readByte(); // Consume '{'. |
| return peeked = PEEKED_BEGIN_OBJECT; |
| default: |
| } |
| |
| int result = peekKeyword(); |
| if (result != PEEKED_NONE) { |
| return result; |
| } |
| |
| result = peekNumber(); |
| if (result != PEEKED_NONE) { |
| return result; |
| } |
| |
| if (!isLiteral(buffer.getByte(0))) { |
| throw syntaxError("Expected value"); |
| } |
| |
| checkLenient(); |
| return peeked = PEEKED_UNQUOTED; |
| } |
| |
| private int peekKeyword() throws IOException { |
| // Figure out which keyword we're matching against by its first character. |
| byte c = buffer.getByte(0); |
| String keyword; |
| String keywordUpper; |
| int peeking; |
| 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; |
| } |
| |
| // Confirm that chars [1..length) match the keyword. |
| int length = keyword.length(); |
| for (int i = 1; i < length; i++) { |
| if (!source.request(i + 1)) { |
| return PEEKED_NONE; |
| } |
| c = buffer.getByte(i); |
| if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) { |
| return PEEKED_NONE; |
| } |
| } |
| |
| if (source.request(length + 1) && isLiteral(buffer.getByte(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. |
| buffer.skip(length); |
| return peeked = peeking; |
| } |
| |
| private int peekNumber() throws IOException { |
| 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 (!source.request(i + 1)) { |
| break; |
| } |
| |
| byte c = buffer.getByte(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. |
| if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative) |
| && (value != 0 || !negative)) { |
| peekedLong = negative ? value : -value; |
| buffer.skip(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; |
| } |
| } |
| |
| private boolean isLiteral(int 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; |
| if (p == PEEKED_UNQUOTED_NAME) { |
| result = nextUnquotedValue(); |
| } else if (p == PEEKED_DOUBLE_QUOTED_NAME) { |
| result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH); |
| } else if (p == PEEKED_SINGLE_QUOTED_NAME) { |
| result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH); |
| } else if (p == PEEKED_BUFFERED_NAME) { |
| result = peekedString; |
| } else { |
| throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath()); |
| } |
| peeked = PEEKED_NONE; |
| pathNames[stackSize - 1] = result; |
| return result; |
| } |
| |
| @Override public int selectName(Options options) throws IOException { |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| if (p < PEEKED_SINGLE_QUOTED_NAME || p > PEEKED_BUFFERED_NAME) { |
| return -1; |
| } |
| if (p == PEEKED_BUFFERED_NAME) { |
| return findName(peekedString, options); |
| } |
| |
| int result = source.select(options.doubleQuoteSuffix); |
| if (result != -1) { |
| peeked = PEEKED_NONE; |
| pathNames[stackSize - 1] = options.strings[result]; |
| |
| return result; |
| } |
| |
| // The next name may be unnecessary escaped. Save the last recorded path name, so that we |
| // can restore the peek state in case we fail to find a match. |
| String lastPathName = pathNames[stackSize - 1]; |
| |
| String nextName = nextName(); |
| result = findName(nextName, options); |
| |
| if (result == -1) { |
| peeked = PEEKED_BUFFERED_NAME; |
| peekedString = nextName; |
| // We can't push the path further, make it seem like nothing happened. |
| pathNames[stackSize - 1] = lastPathName; |
| } |
| |
| return result; |
| } |
| |
| @Override public void skipName() throws IOException { |
| if (failOnUnknown) { |
| throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath()); |
| } |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| if (p == PEEKED_UNQUOTED_NAME) { |
| skipUnquotedValue(); |
| } else if (p == PEEKED_DOUBLE_QUOTED_NAME) { |
| skipQuotedValue(DOUBLE_QUOTE_OR_SLASH); |
| } else if (p == PEEKED_SINGLE_QUOTED_NAME) { |
| skipQuotedValue(SINGLE_QUOTE_OR_SLASH); |
| } else if (p != PEEKED_BUFFERED_NAME) { |
| throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath()); |
| } |
| peeked = PEEKED_NONE; |
| pathNames[stackSize - 1] = "null"; |
| } |
| |
| /** |
| * If {@code name} is in {@code options} this consumes it and returns its index. |
| * Otherwise this returns -1 and no name is consumed. |
| */ |
| private int findName(String name, Options options) { |
| for (int i = 0, size = options.strings.length; i < size; i++) { |
| if (name.equals(options.strings[i])) { |
| peeked = PEEKED_NONE; |
| pathNames[stackSize - 1] = name; |
| |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| @Override public String nextString() throws IOException { |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| String result; |
| if (p == PEEKED_UNQUOTED) { |
| result = nextUnquotedValue(); |
| } else if (p == PEEKED_DOUBLE_QUOTED) { |
| result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH); |
| } else if (p == PEEKED_SINGLE_QUOTED) { |
| result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH); |
| } else if (p == PEEKED_BUFFERED) { |
| result = peekedString; |
| peekedString = null; |
| } else if (p == PEEKED_LONG) { |
| result = Long.toString(peekedLong); |
| } else if (p == PEEKED_NUMBER) { |
| result = buffer.readUtf8(peekedNumberLength); |
| } else { |
| throw new JsonDataException("Expected a string but was " + peek() + " at path " + getPath()); |
| } |
| peeked = PEEKED_NONE; |
| pathIndices[stackSize - 1]++; |
| return result; |
| } |
| |
| /** |
| * If {@code string} is in {@code options} this consumes it and returns its index. |
| * Otherwise this returns -1 and no string is consumed. |
| */ |
| private int findString(String string, Options options) { |
| for (int i = 0, size = options.strings.length; i < size; i++) { |
| if (string.equals(options.strings[i])) { |
| peeked = PEEKED_NONE; |
| pathIndices[stackSize - 1]++; |
| |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| @Override public boolean nextBoolean() throws IOException { |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| if (p == PEEKED_TRUE) { |
| peeked = PEEKED_NONE; |
| pathIndices[stackSize - 1]++; |
| return true; |
| } else if (p == PEEKED_FALSE) { |
| peeked = PEEKED_NONE; |
| pathIndices[stackSize - 1]++; |
| return false; |
| } |
| throw new JsonDataException("Expected a boolean but was " + peek() + " at path " + getPath()); |
| } |
| |
| @Override public double nextDouble() throws IOException { |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| |
| if (p == PEEKED_LONG) { |
| peeked = PEEKED_NONE; |
| pathIndices[stackSize - 1]++; |
| return (double) peekedLong; |
| } |
| |
| if (p == PEEKED_NUMBER) { |
| peekedString = buffer.readUtf8(peekedNumberLength); |
| } else if (p == PEEKED_DOUBLE_QUOTED) { |
| peekedString = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH); |
| } else if (p == PEEKED_SINGLE_QUOTED) { |
| peekedString = nextQuotedValue(SINGLE_QUOTE_OR_SLASH); |
| } else if (p == PEEKED_UNQUOTED) { |
| peekedString = nextUnquotedValue(); |
| } else if (p != PEEKED_BUFFERED) { |
| throw new JsonDataException("Expected a double but was " + peek() + " at path " + getPath()); |
| } |
| |
| peeked = PEEKED_BUFFERED; |
| double result; |
| try { |
| result = Double.parseDouble(peekedString); |
| } catch (NumberFormatException e) { |
| throw new JsonDataException("Expected a double but was " + peekedString |
| + " at path " + getPath()); |
| } |
| if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) { |
| throw new JsonEncodingException("JSON forbids NaN and infinities: " + result |
| + " at path " + getPath()); |
| } |
| peekedString = null; |
| peeked = PEEKED_NONE; |
| pathIndices[stackSize - 1]++; |
| return result; |
| } |
| |
| /** |
| * 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. |
| * |
| * @throws IOException if any unicode escape sequences are malformed. |
| */ |
| private String nextQuotedValue(ByteString runTerminator) throws IOException { |
| StringBuilder builder = null; |
| while (true) { |
| long index = source.indexOfElement(runTerminator); |
| if (index == -1L) throw syntaxError("Unterminated string"); |
| |
| // If we've got an escape character, we're going to need a string builder. |
| if (buffer.getByte(index) == '\\') { |
| if (builder == null) builder = new StringBuilder(); |
| builder.append(buffer.readUtf8(index)); |
| buffer.readByte(); // '\' |
| builder.append(readEscapeCharacter()); |
| continue; |
| } |
| |
| // If it isn't the escape character, it's the quote. Return the string. |
| if (builder == null) { |
| String result = buffer.readUtf8(index); |
| buffer.readByte(); // Consume the quote character. |
| return result; |
| } else { |
| builder.append(buffer.readUtf8(index)); |
| buffer.readByte(); // Consume the quote character. |
| return builder.toString(); |
| } |
| } |
| } |
| |
| /** Returns an unquoted value as a string. */ |
| private String nextUnquotedValue() throws IOException { |
| long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS); |
| return i != -1 ? buffer.readUtf8(i) : buffer.readUtf8(); |
| } |
| |
| private void skipQuotedValue(ByteString runTerminator) throws IOException { |
| while (true) { |
| long index = source.indexOfElement(runTerminator); |
| if (index == -1L) throw syntaxError("Unterminated string"); |
| |
| if (buffer.getByte(index) == '\\') { |
| buffer.skip(index + 1); |
| readEscapeCharacter(); |
| } else { |
| buffer.skip(index + 1); |
| return; |
| } |
| } |
| } |
| |
| private void skipUnquotedValue() throws IOException { |
| long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS); |
| buffer.skip(i != -1L ? i : buffer.size()); |
| } |
| |
| @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 JsonDataException("Expected an int but was " + peekedLong |
| + " at path " + getPath()); |
| } |
| peeked = PEEKED_NONE; |
| pathIndices[stackSize - 1]++; |
| return result; |
| } |
| |
| if (p == PEEKED_NUMBER) { |
| peekedString = buffer.readUtf8(peekedNumberLength); |
| } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED) { |
| peekedString = p == PEEKED_DOUBLE_QUOTED |
| ? nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) |
| : nextQuotedValue(SINGLE_QUOTE_OR_SLASH); |
| try { |
| result = Integer.parseInt(peekedString); |
| peeked = PEEKED_NONE; |
| pathIndices[stackSize - 1]++; |
| return result; |
| } catch (NumberFormatException ignored) { |
| // Fall back to parse as a double below. |
| } |
| } else if (p != PEEKED_BUFFERED) { |
| throw new JsonDataException("Expected an int but was " + peek() + " at path " + getPath()); |
| } |
| |
| peeked = PEEKED_BUFFERED; |
| double asDouble; |
| try { |
| asDouble = Double.parseDouble(peekedString); |
| } catch (NumberFormatException e) { |
| throw new JsonDataException("Expected an int but was " + peekedString |
| + " at path " + getPath()); |
| } |
| result = (int) asDouble; |
| if (result != asDouble) { // Make sure no precision was lost casting to 'int'. |
| throw new JsonDataException("Expected an int but was " + peekedString |
| + " at path " + getPath()); |
| } |
| peekedString = null; |
| peeked = PEEKED_NONE; |
| pathIndices[stackSize - 1]++; |
| return result; |
| } |
| |
| @Override public void close() throws IOException { |
| peeked = PEEKED_NONE; |
| scopes[0] = JsonScope.CLOSED; |
| stackSize = 1; |
| buffer.clear(); |
| source.close(); |
| } |
| |
| @Override public void skipValue() throws IOException { |
| if (failOnUnknown) { |
| throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath()); |
| } |
| int count = 0; |
| do { |
| int p = peeked; |
| if (p == PEEKED_NONE) { |
| p = doPeek(); |
| } |
| |
| if (p == PEEKED_BEGIN_ARRAY) { |
| pushScope(JsonScope.EMPTY_ARRAY); |
| count++; |
| } else if (p == PEEKED_BEGIN_OBJECT) { |
| pushScope(JsonScope.EMPTY_OBJECT); |
| count++; |
| } else if (p == PEEKED_END_ARRAY) { |
| count--; |
| if (count < 0) { |
| throw new JsonDataException( |
| "Expected a value but was " + peek() + " at path " + getPath()); |
| } |
| stackSize--; |
| } else if (p == PEEKED_END_OBJECT) { |
| count--; |
| if (count < 0) { |
| throw new JsonDataException( |
| "Expected a value but was " + peek() + " at path " + getPath()); |
| } |
| stackSize--; |
| } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) { |
| skipUnquotedValue(); |
| } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) { |
| skipQuotedValue(DOUBLE_QUOTE_OR_SLASH); |
| } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) { |
| skipQuotedValue(SINGLE_QUOTE_OR_SLASH); |
| } else if (p == PEEKED_NUMBER) { |
| buffer.skip(peekedNumberLength); |
| } else if (p == PEEKED_EOF) { |
| throw new JsonDataException( |
| "Expected a value but was " + peek() + " at path " + getPath()); |
| } |
| peeked = PEEKED_NONE; |
| } while (count != 0); |
| |
| pathIndices[stackSize - 1]++; |
| pathNames[stackSize - 1] = "null"; |
| } |
| |
| /** |
| * 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.getByte(0)}. |
| */ |
| 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. |
| */ |
| int p = 0; |
| while (source.request(p + 1)) { |
| int c = buffer.getByte(p++); |
| if (c == '\n' || c == ' ' || c == '\r' || c == '\t') { |
| continue; |
| } |
| |
| buffer.skip(p - 1); |
| if (c == '/') { |
| if (!source.request(2)) { |
| return c; |
| } |
| |
| checkLenient(); |
| byte peek = buffer.getByte(1); |
| switch (peek) { |
| case '*': |
| // skip a /* c-style comment */ |
| buffer.readByte(); // '/' |
| buffer.readByte(); // '*' |
| if (!skipToEndOfBlockComment()) { |
| throw syntaxError("Unterminated comment"); |
| } |
| p = 0; |
| continue; |
| |
| case '/': |
| // skip a // end-of-line comment |
| buffer.readByte(); // '/' |
| buffer.readByte(); // '/' |
| skipToEndOfLine(); |
| p = 0; |
| continue; |
| |
| default: |
| return c; |
| } |
| } else if (c == '#') { |
| // Skip a # hash end-of-line comment. The JSON RFC doesn't specify this behaviour, but it's |
| // required to parse existing documents. |
| checkLenient(); |
| skipToEndOfLine(); |
| p = 0; |
| } else { |
| return c; |
| } |
| } |
| if (throwOnEof) { |
| throw new EOFException("End of input"); |
| } else { |
| return -1; |
| } |
| } |
| |
| private void checkLenient() throws IOException { |
| 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 { |
| long index = source.indexOfElement(LINEFEED_OR_CARRIAGE_RETURN); |
| buffer.skip(index != -1 ? index + 1 : buffer.size()); |
| } |
| |
| /** |
| * Skips through the next closing block comment. |
| */ |
| private boolean skipToEndOfBlockComment() throws IOException { |
| long index = source.indexOf(CLOSING_BLOCK_COMMENT); |
| boolean found = index != -1; |
| buffer.skip(found ? index + CLOSING_BLOCK_COMMENT.size() : buffer.size()); |
| return found; |
| } |
| |
| |
| @Override public String toString() { |
| return "JsonReader(" + source + ")"; |
| } |
| |
| /** |
| * 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 IOException if any unicode escape sequences are malformed. |
| */ |
| private char readEscapeCharacter() throws IOException { |
| if (!source.request(1)) { |
| throw syntaxError("Unterminated escape sequence"); |
| } |
| |
| byte escaped = buffer.readByte(); |
| switch (escaped) { |
| case 'u': |
| if (!source.request(4)) { |
| throw new EOFException("Unterminated escape sequence at path " + getPath()); |
| } |
| // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16); |
| char result = 0; |
| for (int i = 0, end = i + 4; i < end; i++) { |
| byte c = buffer.getByte(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("\\u" + buffer.readUtf8(4)); |
| } |
| } |
| buffer.skip(4); |
| return result; |
| |
| case 't': |
| return '\t'; |
| |
| case 'b': |
| return '\b'; |
| |
| case 'n': |
| return '\n'; |
| |
| case 'r': |
| return '\r'; |
| |
| case 'f': |
| return '\f'; |
| |
| case '\n': |
| case '\'': |
| case '"': |
| case '\\': |
| case '/': |
| return (char) escaped; |
| |
| default: |
| if (!lenient) throw syntaxError("Invalid escape sequence: \\" + (char) escaped); |
| return (char) escaped; |
| } |
| } |
| |
| } |