Support up to 255 levels of nesting. (#349)

Closes: https://github.com/square/moshi/issues/348
This commit is contained in:
Jesse Wilson
2018-01-07 12:17:00 -05:00
committed by GitHub
parent 8cde0e5d72
commit 0a6e836762
8 changed files with 70 additions and 43 deletions

View File

@@ -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,8 +202,13 @@ public abstract class JsonReader implements Closeable {
final void pushScope(int newTop) {
if (stackSize == scopes.length) {
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;
}

View File

@@ -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);

View File

@@ -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<Object> 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<String, Object> map = new LinkedHashTreeMap<>();
add(map);
stack[stackSize] = map;

View File

@@ -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;
}

View File

@@ -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));
}
}

View File

@@ -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?");
}
}

View File

@@ -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?");
}
}

View File

@@ -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.");
}