Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1ba41714 authored by Jesse Wilson's avatar Jesse Wilson
Browse files

Adding JsonReader.setLenient() to handle malformed JSON strings.

Also replacing setIndentSpaces() with a more general purpose method,
setIndent().

Change-Id: I64fbe4901aec23de5392362c1d40b77bc2b5566b
parent 1ddf340b
Loading
Loading
Loading
Loading
+19 −4
Original line number Diff line number Diff line
@@ -180354,6 +180354,19 @@
<exception name="IOException" type="java.io.IOException">
</exception>
</method>
<method name="setLenient"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="lenient" type="boolean">
</parameter>
</method>
<method name="skipValue"
 return="void"
 abstract="false"
@@ -180424,6 +180437,8 @@
 deprecated="not deprecated"
 visibility="public"
>
<implements name="java.io.Closeable">
</implements>
<constructor name="JsonWriter"
 type="android.util.JsonWriter"
 static="false"
@@ -180540,7 +180555,7 @@
<exception name="IOException" type="java.io.IOException">
</exception>
</method>
<method name="setIndentSpaces"
<method name="setIndent"
 return="void"
 abstract="false"
 native="false"
@@ -180550,7 +180565,7 @@
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="indent" type="int">
<parameter name="indent" type="java.lang.String">
</parameter>
</method>
<method name="value"
@@ -272628,7 +272643,7 @@
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="loop" type="boolean">
<parameter name="disable" type="boolean">
</parameter>
<exception name="SocketException" type="java.net.SocketException">
</exception>
@@ -274187,7 +274202,7 @@
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="value" type="boolean">
<parameter name="keepAlive" type="boolean">
</parameter>
<exception name="SocketException" type="java.net.SocketException">
</exception>
+103 −61
Original line number Diff line number Diff line
@@ -172,6 +172,9 @@ public final class JsonReader implements Closeable {
    /** The input JSON. */
    private final Reader in;

    /** True to accept non-spec compliant JSON */
    private boolean lenient = false;

    /**
     * Use a manual buffer to easily read and unread upcoming characters, and
     * also so we can create strings without an intermediate StringBuilder.
@@ -207,9 +210,6 @@ public final class JsonReader implements Closeable {
    /** The text of the next literal value. */
    private String value;

    // TODO: make this parser strict and offer an optional lenient mode?
    // TODO: document how this reader is non-strict

    /**
     * Creates a new instance that reads a JSON-encoded stream from {@code in}.
     */
@@ -220,6 +220,31 @@ public final class JsonReader implements Closeable {
        this.in = in;
    }

    /**
     * Configure this parser to be  be liberal in what it accepts. By default,
     * this parser is strict and only accepts JSON as specified by <a
     * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the
     * parser to lenient causes it to ignore the following syntax errors:
     *
     * <ul>
     *   <li>End of line comments starting with {@code //} or {@code #} and
     *       ending with a newline character.
     *   <li>C-style comments starting with {@code /*} and ending with
     *       {@code *}{@code /}. Such comments may not be nested.
     *   <li>Names that are unquoted or {@code 'single quoted'}.
     *   <li>Strings that are unquoted or {@code 'single quoted'}.
     *   <li>Array elements separated by {@code ;} instead of {@code ,}.
     *   <li>Unnecessary array separators. These are interpreted as if null
     *       was the omitted value.
     *   <li>Names and values separated by {@code =} or {@code =>} instead of
     *       {@code :}.
     *   <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
     * </ul>
     */
    public void setLenient(boolean lenient) {
        this.lenient = lenient;
    }

    /**
     * Consumes the next token from the JSON stream and asserts that it is the
     * beginning of a new array.
@@ -253,11 +278,12 @@ public final class JsonReader implements Closeable {
    }

    /**
     * Consumes {@code token}.
     * Consumes {@code expected}.
     */
    private void expect(JsonToken token) throws IOException {
        if (quickPeek() != token) {
            throw new IllegalStateException("Expected " + token + " but was " + peek());
    private void expect(JsonToken expected) throws IOException {
        quickPeek();
        if (token != expected) {
            throw new IllegalStateException("Expected " + expected + " but was " + peek());
        }
        advance();
    }
@@ -266,8 +292,8 @@ public final class JsonReader implements Closeable {
     * Returns true if the current array or object has another element.
     */
    public boolean hasNext() throws IOException {
        JsonToken peek = quickPeek();
        return peek != JsonToken.END_OBJECT && peek != JsonToken.END_ARRAY;
        quickPeek();
        return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY;
    }

    /**
@@ -285,11 +311,8 @@ public final class JsonReader implements Closeable {

    /**
     * Ensures that a token is ready. After this call either {@code token} or
     * {@code value} will be non-null.
     *
     * @return the type of the next token, of {@code null} if it is unknown. For
     *     a definitive result, use {@link #peek()} which decodes the token
     *     type.
     * {@code value} will be non-null. To ensure {@code token} has a definitive
     * value, use {@link #peek()}
     */
    private JsonToken quickPeek() throws IOException {
        if (hasToken) {
@@ -347,7 +370,8 @@ public final class JsonReader implements Closeable {
     *     name.
     */
    public String nextName() throws IOException {
        if (quickPeek() != JsonToken.NAME) {
        quickPeek();
        if (token != JsonToken.NAME) {
            throw new IllegalStateException("Expected a name but was " + peek());
        }
        String result = name;
@@ -364,8 +388,8 @@ public final class JsonReader implements Closeable {
     *     this reader is closed.
     */
    public String nextString() throws IOException {
        JsonToken peek = peek();
        if (value == null || (peek != JsonToken.STRING && peek != JsonToken.NUMBER)) {
        peek();
        if (value == null || (token != JsonToken.STRING && token != JsonToken.NUMBER)) {
            throw new IllegalStateException("Expected a string but was " + peek());
        }

@@ -382,8 +406,8 @@ public final class JsonReader implements Closeable {
     *     this reader is closed.
     */
    public boolean nextBoolean() throws IOException {
        JsonToken peek = quickPeek();
        if (value == null || peek == JsonToken.STRING) {
        quickPeek();
        if (value == null || token == JsonToken.STRING) {
            throw new IllegalStateException("Expected a boolean but was " + peek());
        }

@@ -408,8 +432,8 @@ public final class JsonReader implements Closeable {
     *     reader is closed.
     */
    public void nextNull() throws IOException {
        JsonToken peek = quickPeek();
        if (value == null || peek == JsonToken.STRING) {
        quickPeek();
        if (value == null || token == JsonToken.STRING) {
            throw new IllegalStateException("Expected null but was " + peek());
        }

@@ -570,37 +594,44 @@ public final class JsonReader implements Closeable {

    private JsonToken nextInArray(boolean firstElement) throws IOException {
        if (firstElement) {
            replaceTop(JsonScope.NONEMPTY_ARRAY);
        } else {
            /* Look for a comma before each element after the first element. */
            switch (nextNonWhitespace()) {
                case ']':
                    pop();
                    hasToken = true;
                    return token = JsonToken.END_ARRAY;
                case ',':
                case ';':
                    /* a separator without a value first means "null". */
                    // TODO: forbid this in strict mode
                    hasToken = true;
                    return token = JsonToken.NULL;
                    checkLenient(); // fall-through
                case ',':
                    break;
                default:
                    replaceTop(JsonScope.NONEMPTY_ARRAY);
                    pos--;
                    throw syntaxError("Unterminated array");
            }
        } else {
        }

        switch (nextNonWhitespace()) {
            case ']':
                if (firstElement) {
                    pop();
                    hasToken = true;
                    return token = JsonToken.END_ARRAY;
                case ',':
                }
                // fall-through to handle ",]"
            case ';':
                    break;
            case ',':
                /* In lenient mode, a 0-length literal means 'null' */
                checkLenient();
                pos--;
                hasToken = true;
                value = "null";
                return token = JsonToken.NULL;
            default:
                    throw syntaxError("Unterminated array");
            }
        }

                pos--;
                return nextValue();
        }
    }

    private JsonToken nextInObject(boolean firstElement) throws IOException {
        /*
@@ -636,10 +667,12 @@ public final class JsonReader implements Closeable {
        int quote = nextNonWhitespace();
        switch (quote) {
            case '\'':
                checkLenient(); // fall-through
            case '"':
                name = nextString((char) quote);
                break;
            default:
                checkLenient();
                pos--;
                name = nextLiteral();
                if (name.isEmpty()) {
@@ -653,20 +686,22 @@ public final class JsonReader implements Closeable {
    }

    private JsonToken objectValue() throws IOException {
        // TODO: accept only ":" in strict mode

        /*
         * Read the name/value separator. Usually a colon ':', an equals sign
         * '=', or an arrow "=>". The last two are bogus but we include them
         * because that's what org.json does.
         * Read the name/value separator. Usually a colon ':'. In lenient mode
         * we also accept an equals sign '=', or an arrow "=>".
         */
        int separator = nextNonWhitespace();
        if (separator != ':' && separator != '=') {
            throw syntaxError("Expected ':'");
        }
        if (separator == '=' && (pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
        switch (nextNonWhitespace()) {
            case ':':
                break;
            case '=':
                checkLenient();
                if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
                    pos++;
                }
                break;
            default:
                throw syntaxError("Expected ':'");
        }

        replaceTop(JsonScope.NONEMPTY_OBJECT);
        return nextValue();
@@ -686,6 +721,7 @@ public final class JsonReader implements Closeable {
                return token = JsonToken.BEGIN_ARRAY;

            case '\'':
                checkLenient(); // fall-through
            case '"':
                value = nextString((char) c);
                hasToken = true;
@@ -722,8 +758,6 @@ public final class JsonReader implements Closeable {
    }

    private int nextNonWhitespace() throws IOException {
        // TODO: no comments in strict mode

        while (pos < limit || fillBuffer(1)) {
            int c = buffer[pos++];
            switch (c) {
@@ -738,6 +772,7 @@ public final class JsonReader implements Closeable {
                        return c;
                    }

                    checkLenient();
                    char peek = buffer[pos];
                    switch (peek) {
                        case '*':
@@ -765,6 +800,7 @@ public final class JsonReader implements Closeable {
                     * specify this behaviour, but it's required to parse
                     * existing documents. See http://b/2571423.
                     */
                    checkLenient();
                    skipToEndOfLine();
                    continue;

@@ -776,6 +812,12 @@ public final class JsonReader implements Closeable {
        throw syntaxError("End of input");
    }

    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
@@ -853,9 +895,6 @@ public final class JsonReader implements Closeable {
     * does not consume the delimiter character.
     */
    private String nextLiteral() throws IOException {
        // TODO: use a much smaller set of permitted literal characters in strict mode;
        //       these characters are derived from org.json's lenient mode

        StringBuilder builder = null;
        do {
            /* the index of the first character not yet appended to the builder. */
@@ -863,17 +902,19 @@ public final class JsonReader implements Closeable {
            while (pos < limit) {
                int c = buffer[pos++];
                switch (c) {
                    case '/':
                    case '\\':
                    case ';':
                    case '#':
                    case '=':
                        checkLenient(); // fall-through

                    case '{':
                    case '}':
                    case '[':
                    case ']':
                    case '/':
                    case '\\':
                    case ':':
                    case '=':
                    case ',':
                    case ';':
                    case '#':
                    case ' ':
                    case '\t':
                    case '\f':
@@ -965,7 +1006,7 @@ public final class JsonReader implements Closeable {
    /**
     * Assigns {@code nextToken} based on the value of {@code nextValue}.
     */
    private void decodeLiteral() {
    private void decodeLiteral() throws IOException {
        if (value.equalsIgnoreCase("null")) {
            token = JsonToken.NULL;
        } else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
@@ -975,7 +1016,8 @@ public final class JsonReader implements Closeable {
                Double.parseDouble(value); // this work could potentially be cached
                token = JsonToken.NUMBER;
            } catch (NumberFormatException ignored) {
                /* an unquoted string. This document is not well-formed! */
                // this must be an unquoted string
                checkLenient();
                token = JsonToken.STRING;
            }
        }
+13 −16
Original line number Diff line number Diff line
@@ -16,10 +16,10 @@

package android.util;

import java.io.Closeable;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
@@ -117,7 +117,7 @@ import java.util.List;
 * Instances of this class are not thread safe. Calls that would result in a
 * malformed JSON string will fail with an {@link IllegalStateException}.
 */
public final class JsonWriter {
public final class JsonWriter implements Closeable {

    /** The output data, containing at most one top-level array or object. */
    private final Writer out;
@@ -151,22 +151,19 @@ public final class JsonWriter {
    }

    /**
     * Sets the number of spaces to indent each line in the encoded document.
     * If {@code indent == 0} the encoded document will be compact. If {@code
     * indent > 0}, the encoded document will be more human-readable.
     * Sets the indentation string to be repeated for each level of indentation
     * in the encoded document. If {@code indent.isEmpty()} the encoded document
     * will be compact. Otherwise the encoded document will be more
     * human-readable.
     *
     * @param indent a string containing only whitespace.
     */
    public void setIndentSpaces(int indent) {
        if (indent < 0) {
            throw new IllegalArgumentException("indent < 0");
        }

        if (indent > 0) {
            char[] indentChars = new char[indent];
            Arrays.fill(indentChars, ' ');
            this.indent = new String(indentChars);
    public void setIndent(String indent) {
        if (indent.isEmpty()) {
            this.indent = null;
            this.separator = ":";
        } else {
            this.indent = null;
            this.indent = indent;
            this.separator = ": ";
        }
    }
+268 −0
Original line number Diff line number Diff line
@@ -32,6 +32,14 @@ public final class JsonReaderTest extends TestCase {
        assertEquals(JsonToken.END_DOCUMENT, reader.peek());
    }

    public void testReadEmptyArray() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("[]"));
        reader.beginArray();
        assertFalse(reader.hasNext());
        reader.endArray();
        assertEquals(JsonToken.END_DOCUMENT, reader.peek());
    }

    public void testReadObject() throws IOException {
        JsonReader reader = new JsonReader(new StringReader(
                "{\"a\": \"android\", \"b\": \"banana\"}"));
@@ -44,6 +52,14 @@ public final class JsonReaderTest extends TestCase {
        assertEquals(JsonToken.END_DOCUMENT, reader.peek());
    }

    public void testReadEmptyObject() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("{}"));
        reader.beginObject();
        assertFalse(reader.hasNext());
        reader.endObject();
        assertEquals(JsonToken.END_DOCUMENT, reader.peek());
    }

    public void testSkipObject() throws IOException {
        JsonReader reader = new JsonReader(new StringReader(
                "{\"a\": { \"c\": [], \"d\": [true, true, {}] }, \"b\": \"banana\"}"));
@@ -412,4 +428,256 @@ public final class JsonReaderTest extends TestCase {
        } catch (IllegalStateException expected) {
        }
    }

    public void testStrictNameValueSeparator() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("{\"a\"=true}"));
        reader.beginObject();
        assertEquals("a", reader.nextName());
        try {
            reader.nextBoolean();
            fail();
        } catch (IOException expected) {
        }

        reader = new JsonReader(new StringReader("{\"a\"=>true}"));
        reader.beginObject();
        assertEquals("a", reader.nextName());
        try {
            reader.nextBoolean();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testLenientNameValueSeparator() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("{\"a\"=true}"));
        reader.setLenient(true);
        reader.beginObject();
        assertEquals("a", reader.nextName());
        assertEquals(true, reader.nextBoolean());

        reader = new JsonReader(new StringReader("{\"a\"=>true}"));
        reader.setLenient(true);
        reader.beginObject();
        assertEquals("a", reader.nextName());
        assertEquals(true, reader.nextBoolean());
    }

    public void testStrictComments() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("[// comment \n true]"));
        reader.beginArray();
        try {
            reader.nextBoolean();
            fail();
        } catch (IOException expected) {
        }

        reader = new JsonReader(new StringReader("[# comment \n true]"));
        reader.beginArray();
        try {
            reader.nextBoolean();
            fail();
        } catch (IOException expected) {
        }

        reader = new JsonReader(new StringReader("[/* comment */ true]"));
        reader.beginArray();
        try {
            reader.nextBoolean();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testLenientComments() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("[// comment \n true]"));
        reader.setLenient(true);
        reader.beginArray();
        assertEquals(true, reader.nextBoolean());

        reader = new JsonReader(new StringReader("[# comment \n true]"));
        reader.setLenient(true);
        reader.beginArray();
        assertEquals(true, reader.nextBoolean());

        reader = new JsonReader(new StringReader("[/* comment */ true]"));
        reader.setLenient(true);
        reader.beginArray();
        assertEquals(true, reader.nextBoolean());
    }

    public void testStrictUnquotedNames() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("{a:true}"));
        reader.beginObject();
        try {
            reader.nextName();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testLenientUnquotedNames() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("{a:true}"));
        reader.setLenient(true);
        reader.beginObject();
        assertEquals("a", reader.nextName());
    }

    public void testStrictSingleQuotedNames() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("{'a':true}"));
        reader.beginObject();
        try {
            reader.nextName();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testLenientSingleQuotedNames() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("{'a':true}"));
        reader.setLenient(true);
        reader.beginObject();
        assertEquals("a", reader.nextName());
    }

    public void testStrictUnquotedStrings() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("[a]"));
        reader.beginArray();
        try {
            reader.nextString();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testLenientUnquotedStrings() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("[a]"));
        reader.setLenient(true);
        reader.beginArray();
        assertEquals("a", reader.nextString());
    }

    public void testStrictSingleQuotedStrings() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("['a']"));
        reader.beginArray();
        try {
            reader.nextString();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testLenientSingleQuotedStrings() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("['a']"));
        reader.setLenient(true);
        reader.beginArray();
        assertEquals("a", reader.nextString());
    }

    public void testStrictSemicolonDelimitedArray() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("[true;true]"));
        reader.beginArray();
        try {
            reader.nextBoolean();
            reader.nextBoolean();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testLenientSemicolonDelimitedArray() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("[true;true]"));
        reader.setLenient(true);
        reader.beginArray();
        assertEquals(true, reader.nextBoolean());
        assertEquals(true, reader.nextBoolean());
    }

    public void testStrictSemicolonDelimitedNameValuePair() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("{\"a\":true;\"b\":true}"));
        reader.beginObject();
        assertEquals("a", reader.nextName());
        try {
            reader.nextBoolean();
            reader.nextName();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testLenientSemicolonDelimitedNameValuePair() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("{\"a\":true;\"b\":true}"));
        reader.setLenient(true);
        reader.beginObject();
        assertEquals("a", reader.nextName());
        assertEquals(true, reader.nextBoolean());
        assertEquals("b", reader.nextName());
    }

    public void testStrictUnnecessaryArraySeparators() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("[true,,true]"));
        reader.beginArray();
        assertEquals(true, reader.nextBoolean());
        try {
            reader.nextNull();
            fail();
        } catch (IOException expected) {
        }

        reader = new JsonReader(new StringReader("[,true]"));
        reader.beginArray();
        try {
            reader.nextNull();
            fail();
        } catch (IOException expected) {
        }

        reader = new JsonReader(new StringReader("[true,]"));
        reader.beginArray();
        assertEquals(true, reader.nextBoolean());
        try {
            reader.nextNull();
            fail();
        } catch (IOException expected) {
        }

        reader = new JsonReader(new StringReader("[,]"));
        reader.beginArray();
        try {
            reader.nextNull();
            fail();
        } catch (IOException expected) {
        }
    }

    public void testLenientUnnecessaryArraySeparators() throws IOException {
        JsonReader reader = new JsonReader(new StringReader("[true,,true]"));
        reader.setLenient(true);
        reader.beginArray();
        assertEquals(true, reader.nextBoolean());
        reader.nextNull();
        assertEquals(true, reader.nextBoolean());
        reader.endArray();

        reader = new JsonReader(new StringReader("[,true]"));
        reader.setLenient(true);
        reader.beginArray();
        reader.nextNull();
        assertEquals(true, reader.nextBoolean());
        reader.endArray();

        reader = new JsonReader(new StringReader("[true,]"));
        reader.setLenient(true);
        reader.beginArray();
        assertEquals(true, reader.nextBoolean());
        reader.nextNull();
        reader.endArray();

        reader = new JsonReader(new StringReader("[,]"));
        reader.setLenient(true);
        reader.beginArray();
        reader.nextNull();
        reader.nextNull();
        reader.endArray();
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -346,7 +346,7 @@ public final class JsonWriterTest extends TestCase {
    public void testPrettyPrintObject() throws IOException {
        StringWriter stringWriter = new StringWriter();
        JsonWriter jsonWriter = new JsonWriter(stringWriter);
        jsonWriter.setIndentSpaces(3);
        jsonWriter.setIndent("   ");

        jsonWriter.beginObject();
        jsonWriter.name("a").value(true);
@@ -383,7 +383,7 @@ public final class JsonWriterTest extends TestCase {
    public void testPrettyPrintArray() throws IOException {
        StringWriter stringWriter = new StringWriter();
        JsonWriter jsonWriter = new JsonWriter(stringWriter);
        jsonWriter.setIndentSpaces(3);
        jsonWriter.setIndent("   ");

        jsonWriter.beginArray();
        jsonWriter.value(true);