diff --git a/moshi/src/main/java/com/squareup/moshi/JsonReader.java b/moshi/src/main/java/com/squareup/moshi/JsonReader.java index 1b64b2b..1ecd878 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonReader.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonReader.java @@ -18,6 +18,7 @@ package com.squareup.moshi; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import javax.annotation.CheckReturnValue; @@ -176,13 +177,13 @@ import okio.ByteString; * of this class are not thread safe. */ public abstract class JsonReader implements Closeable { - // The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits - // up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger - // StackOverflowErrors. + // The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will + // grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is + // prone to trigger StackOverflowErrors. int stackSize = 0; - final int[] scopes = new int[32]; - final String[] pathNames = new String[32]; - final int[] pathIndices = new int[32]; + int[] scopes = new int[32]; + String[] pathNames = new String[32]; + int[] pathIndices = new int[32]; /** True to accept non-spec compliant JSON. */ boolean lenient; @@ -201,7 +202,12 @@ public abstract class JsonReader implements Closeable { final void pushScope(int newTop) { if (stackSize == scopes.length) { - throw new JsonDataException("Nesting too deep at " + getPath()); + if (stackSize == 256) { + throw new JsonDataException("Nesting too deep at " + getPath()); + } + scopes = Arrays.copyOf(scopes, scopes.length * 2); + pathNames = Arrays.copyOf(pathNames, pathNames.length * 2); + pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2); } scopes[stackSize++] = newTop; } diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java index 97838bc..221f259 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Writer.java @@ -101,6 +101,7 @@ final class JsonUtf8Writer extends JsonWriter { */ private JsonWriter open(int empty, String openBracket) throws IOException { beforeValue(); + checkStack(); pushScope(empty); pathIndices[stackSize - 1] = 0; sink.writeUtf8(openBracket); diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java index 79fc9b8..cc532f2 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonValueWriter.java @@ -31,7 +31,7 @@ import static java.lang.Double.POSITIVE_INFINITY; /** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */ final class JsonValueWriter extends JsonWriter { - private final Object[] stack = new Object[32]; + Object[] stack = new Object[32]; private @Nullable String deferredName; JsonValueWriter() { @@ -47,9 +47,7 @@ final class JsonValueWriter extends JsonWriter { } @Override public JsonWriter beginArray() throws IOException { - if (stackSize == stack.length) { - throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?"); - } + checkStack(); List list = new ArrayList<>(); add(list); stack[stackSize] = list; @@ -69,9 +67,7 @@ final class JsonValueWriter extends JsonWriter { } @Override public JsonWriter beginObject() throws IOException { - if (stackSize == stack.length) { - throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?"); - } + checkStack(); Map map = new LinkedHashTreeMap<>(); add(map); stack[stackSize] = map; diff --git a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java b/moshi/src/main/java/com/squareup/moshi/JsonWriter.java index 067eae0..70e9513 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonWriter.java @@ -18,6 +18,7 @@ package com.squareup.moshi; import java.io.Closeable; import java.io.Flushable; import java.io.IOException; +import java.util.Arrays; import javax.annotation.CheckReturnValue; import javax.annotation.Nullable; import okio.BufferedSink; @@ -121,13 +122,13 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT; * malformed JSON string will fail with an {@link IllegalStateException}. */ public abstract class JsonWriter implements Closeable, Flushable { - // The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits - // up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger - // StackOverflowErrors. + // The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will + // grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is + // prone to trigger StackOverflowErrors. int stackSize = 0; - final int[] scopes = new int[32]; - final String[] pathNames = new String[32]; - final int[] pathIndices = new int[32]; + int[] scopes = new int[32]; + String[] pathNames = new String[32]; + int[] pathIndices = new int[32]; /** * A string containing a full set of spaces for a single level of indentation, or null for no @@ -155,10 +156,26 @@ public abstract class JsonWriter implements Closeable, Flushable { return scopes[stackSize - 1]; } - final void pushScope(int newTop) { - if (stackSize == scopes.length) { + /** Before pushing a value on the stack this confirms that the stack has capacity. */ + final boolean checkStack() { + if (stackSize != scopes.length) return false; + + if (stackSize == 256) { throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?"); } + + scopes = Arrays.copyOf(scopes, scopes.length * 2); + pathNames = Arrays.copyOf(pathNames, pathNames.length * 2); + pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2); + if (this instanceof JsonValueWriter) { + ((JsonValueWriter) this).stack = + Arrays.copyOf(((JsonValueWriter) this).stack, ((JsonValueWriter) this).stack.length * 2); + } + + return true; + } + + final void pushScope(int newTop) { scopes[stackSize++] = newTop; } diff --git a/moshi/src/test/java/com/squareup/moshi/JsonUtf8ReaderTest.java b/moshi/src/test/java/com/squareup/moshi/JsonUtf8ReaderTest.java index 3b8b68a..b9a6bf7 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonUtf8ReaderTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonUtf8ReaderTest.java @@ -1023,17 +1023,15 @@ public final class JsonUtf8ReaderTest { } @Test public void tooDeeplyNestedArrays() throws IOException { - JsonReader reader = newReader( - "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"); - for (int i = 0; i < 31; i++) { + JsonReader reader = newReader(repeat("[", 256) + repeat("]", 256)); + for (int i = 0; i < 255; i++) { reader.beginArray(); } try { reader.beginArray(); fail(); } catch (JsonDataException expected) { - assertThat(expected).hasMessage("Nesting too deep at $[0][0][0][0][0][0][0][0][0][0][0][0][0]" - + "[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]"); + assertThat(expected).hasMessage("Nesting too deep at $" + repeat("[0]", 255)); } } @@ -1041,12 +1039,12 @@ public final class JsonUtf8ReaderTest { // Build a JSON document structured like {"a":{"a":{"a":{"a":true}}}}, but 31 levels deep. String array = "{\"a\":%s}"; String json = "true"; - for (int i = 0; i < 32; i++) { + for (int i = 0; i < 256; i++) { json = String.format(array, json); } JsonReader reader = newReader(json); - for (int i = 0; i < 31; i++) { + for (int i = 0; i < 255; i++) { reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); } @@ -1054,8 +1052,7 @@ public final class JsonUtf8ReaderTest { reader.beginObject(); fail(); } catch (JsonDataException expected) { - assertThat(expected).hasMessage( - "Nesting too deep at $.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a"); + assertThat(expected).hasMessage("Nesting too deep at $" + repeat(".a", 255)); } } diff --git a/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java b/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java index 35538c6..5bf1b75 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java @@ -25,6 +25,7 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; +import static com.squareup.moshi.TestUtil.repeat; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; @@ -434,15 +435,15 @@ public final class JsonWriterTest { @Test public void tooDeepNestingArrays() throws IOException { JsonWriter writer = factory.newWriter(); - for (int i = 0; i < 31; i++) { + for (int i = 0; i < 255; i++) { writer.beginArray(); } try { writer.beginArray(); fail(); } catch (JsonDataException expected) { - assertThat(expected).hasMessage("Nesting too deep at $[0][0][0][0][0][0][0][0][0][0][0][0][0]" - + "[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]: circular reference?"); + assertThat(expected).hasMessage("Nesting too deep at $" + + repeat("[0]", 255) + ": circular reference?"); } } @@ -464,7 +465,7 @@ public final class JsonWriterTest { @Test public void tooDeepNestingObjects() throws IOException { JsonWriter writer = factory.newWriter(); - for (int i = 0; i < 31; i++) { + for (int i = 0; i < 255; i++) { writer.beginObject(); writer.name("a"); } @@ -472,8 +473,8 @@ public final class JsonWriterTest { writer.beginObject(); fail(); } catch (JsonDataException expected) { - assertThat(expected).hasMessage("Nesting too deep at $.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a." - + "a.a.a.a.a.a.a.a.a.a.a.a: circular reference?"); + assertThat(expected).hasMessage("Nesting too deep at $" + + repeat(".a", 255) + ": circular reference?"); } } diff --git a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java index 33e7dbd..97909a5 100644 --- a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java +++ b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java @@ -38,6 +38,7 @@ import okio.Buffer; import org.junit.Test; import static com.squareup.moshi.TestUtil.newReader; +import static com.squareup.moshi.TestUtil.repeat; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; @@ -971,8 +972,8 @@ public final class MoshiTest { moshi.adapter(Object.class).toJson(map); fail(); } catch (JsonDataException expected) { - assertThat(expected).hasMessage("Nesting too deep at $.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a." - + "a.a.a.a.a.a.a.a.a.a.a.a: circular reference?"); + assertThat(expected).hasMessage("Nesting too deep at $" + + repeat(".a", 255) + ": circular reference?"); } } @@ -984,8 +985,8 @@ public final class MoshiTest { moshi.adapter(Object.class).toJson(list); fail(); } catch (JsonDataException expected) { - assertThat(expected).hasMessage("Nesting too deep at $[0][0][0][0][0][0][0][0][0][0][0][0][0]" - + "[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]: circular reference?"); + assertThat(expected).hasMessage("Nesting too deep at $" + + repeat("[0]", 255) + ": circular reference?"); } } @@ -999,8 +1000,8 @@ public final class MoshiTest { moshi.adapter(Object.class).toJson(list); fail(); } catch (JsonDataException expected) { - assertThat(expected).hasMessage("Nesting too deep at $[0].a[0].a[0].a[0].a[0].a[0].a[0].a[0]." - + "a[0].a[0].a[0].a[0].a[0].a[0].a[0].a[0]: circular reference?"); + assertThat(expected).hasMessage("Nesting too deep at $[0]" + + repeat(".a[0]", 127) + ": circular reference?"); } } diff --git a/moshi/src/test/java/com/squareup/moshi/TestUtil.java b/moshi/src/test/java/com/squareup/moshi/TestUtil.java index 0a64af2..f395712 100644 --- a/moshi/src/test/java/com/squareup/moshi/TestUtil.java +++ b/moshi/src/test/java/com/squareup/moshi/TestUtil.java @@ -30,6 +30,14 @@ final class TestUtil { return new String(array); } + static String repeat(String s, int count) { + StringBuilder result = new StringBuilder(s.length() * count); + for (int i = 0; i < count; i++) { + result.append(s); + } + return result.toString(); + } + private TestUtil() { throw new AssertionError("No instances."); }