mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
Support up to 255 levels of nesting. (#349)
Closes: https://github.com/square/moshi/issues/348
This commit is contained in:
@@ -18,6 +18,7 @@ package com.squareup.moshi;
|
|||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.annotation.CheckReturnValue;
|
import javax.annotation.CheckReturnValue;
|
||||||
@@ -176,13 +177,13 @@ import okio.ByteString;
|
|||||||
* of this class are not thread safe.
|
* of this class are not thread safe.
|
||||||
*/
|
*/
|
||||||
public abstract class JsonReader implements Closeable {
|
public abstract class JsonReader implements Closeable {
|
||||||
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
|
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
|
||||||
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
|
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
|
||||||
// StackOverflowErrors.
|
// prone to trigger StackOverflowErrors.
|
||||||
int stackSize = 0;
|
int stackSize = 0;
|
||||||
final int[] scopes = new int[32];
|
int[] scopes = new int[32];
|
||||||
final String[] pathNames = new String[32];
|
String[] pathNames = new String[32];
|
||||||
final int[] pathIndices = new int[32];
|
int[] pathIndices = new int[32];
|
||||||
|
|
||||||
/** True to accept non-spec compliant JSON. */
|
/** True to accept non-spec compliant JSON. */
|
||||||
boolean lenient;
|
boolean lenient;
|
||||||
@@ -201,7 +202,12 @@ public abstract class JsonReader implements Closeable {
|
|||||||
|
|
||||||
final void pushScope(int newTop) {
|
final void pushScope(int newTop) {
|
||||||
if (stackSize == scopes.length) {
|
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;
|
scopes[stackSize++] = newTop;
|
||||||
}
|
}
|
||||||
|
@@ -101,6 +101,7 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||||||
*/
|
*/
|
||||||
private JsonWriter open(int empty, String openBracket) throws IOException {
|
private JsonWriter open(int empty, String openBracket) throws IOException {
|
||||||
beforeValue();
|
beforeValue();
|
||||||
|
checkStack();
|
||||||
pushScope(empty);
|
pushScope(empty);
|
||||||
pathIndices[stackSize - 1] = 0;
|
pathIndices[stackSize - 1] = 0;
|
||||||
sink.writeUtf8(openBracket);
|
sink.writeUtf8(openBracket);
|
||||||
|
@@ -31,7 +31,7 @@ import static java.lang.Double.POSITIVE_INFINITY;
|
|||||||
|
|
||||||
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
|
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
|
||||||
final class JsonValueWriter extends JsonWriter {
|
final class JsonValueWriter extends JsonWriter {
|
||||||
private final Object[] stack = new Object[32];
|
Object[] stack = new Object[32];
|
||||||
private @Nullable String deferredName;
|
private @Nullable String deferredName;
|
||||||
|
|
||||||
JsonValueWriter() {
|
JsonValueWriter() {
|
||||||
@@ -47,9 +47,7 @@ final class JsonValueWriter extends JsonWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override public JsonWriter beginArray() throws IOException {
|
@Override public JsonWriter beginArray() throws IOException {
|
||||||
if (stackSize == stack.length) {
|
checkStack();
|
||||||
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
|
|
||||||
}
|
|
||||||
List<Object> list = new ArrayList<>();
|
List<Object> list = new ArrayList<>();
|
||||||
add(list);
|
add(list);
|
||||||
stack[stackSize] = list;
|
stack[stackSize] = list;
|
||||||
@@ -69,9 +67,7 @@ final class JsonValueWriter extends JsonWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override public JsonWriter beginObject() throws IOException {
|
@Override public JsonWriter beginObject() throws IOException {
|
||||||
if (stackSize == stack.length) {
|
checkStack();
|
||||||
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
|
|
||||||
}
|
|
||||||
Map<String, Object> map = new LinkedHashTreeMap<>();
|
Map<String, Object> map = new LinkedHashTreeMap<>();
|
||||||
add(map);
|
add(map);
|
||||||
stack[stackSize] = map;
|
stack[stackSize] = map;
|
||||||
|
@@ -18,6 +18,7 @@ package com.squareup.moshi;
|
|||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.Flushable;
|
import java.io.Flushable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
import javax.annotation.CheckReturnValue;
|
import javax.annotation.CheckReturnValue;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import okio.BufferedSink;
|
import okio.BufferedSink;
|
||||||
@@ -121,13 +122,13 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
|
|||||||
* malformed JSON string will fail with an {@link IllegalStateException}.
|
* malformed JSON string will fail with an {@link IllegalStateException}.
|
||||||
*/
|
*/
|
||||||
public abstract class JsonWriter implements Closeable, Flushable {
|
public abstract class JsonWriter implements Closeable, Flushable {
|
||||||
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
|
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
|
||||||
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
|
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
|
||||||
// StackOverflowErrors.
|
// prone to trigger StackOverflowErrors.
|
||||||
int stackSize = 0;
|
int stackSize = 0;
|
||||||
final int[] scopes = new int[32];
|
int[] scopes = new int[32];
|
||||||
final String[] pathNames = new String[32];
|
String[] pathNames = new String[32];
|
||||||
final int[] pathIndices = new int[32];
|
int[] pathIndices = new int[32];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A string containing a full set of spaces for a single level of indentation, or null for no
|
* 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];
|
return scopes[stackSize - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
final void pushScope(int newTop) {
|
/** Before pushing a value on the stack this confirms that the stack has capacity. */
|
||||||
if (stackSize == scopes.length) {
|
final boolean checkStack() {
|
||||||
|
if (stackSize != scopes.length) return false;
|
||||||
|
|
||||||
|
if (stackSize == 256) {
|
||||||
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
|
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;
|
scopes[stackSize++] = newTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1023,17 +1023,15 @@ public final class JsonUtf8ReaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test public void tooDeeplyNestedArrays() throws IOException {
|
@Test public void tooDeeplyNestedArrays() throws IOException {
|
||||||
JsonReader reader = newReader(
|
JsonReader reader = newReader(repeat("[", 256) + repeat("]", 256));
|
||||||
"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]");
|
for (int i = 0; i < 255; i++) {
|
||||||
for (int i = 0; i < 31; i++) {
|
|
||||||
reader.beginArray();
|
reader.beginArray();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
reader.beginArray();
|
reader.beginArray();
|
||||||
fail();
|
fail();
|
||||||
} catch (JsonDataException expected) {
|
} catch (JsonDataException expected) {
|
||||||
assertThat(expected).hasMessage("Nesting too deep at $[0][0][0][0][0][0][0][0][0][0][0][0][0]"
|
assertThat(expected).hasMessage("Nesting too deep at $" + repeat("[0]", 255));
|
||||||
+ "[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,12 +1039,12 @@ public final class JsonUtf8ReaderTest {
|
|||||||
// Build a JSON document structured like {"a":{"a":{"a":{"a":true}}}}, but 31 levels deep.
|
// Build a JSON document structured like {"a":{"a":{"a":{"a":true}}}}, but 31 levels deep.
|
||||||
String array = "{\"a\":%s}";
|
String array = "{\"a\":%s}";
|
||||||
String json = "true";
|
String json = "true";
|
||||||
for (int i = 0; i < 32; i++) {
|
for (int i = 0; i < 256; i++) {
|
||||||
json = String.format(array, json);
|
json = String.format(array, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonReader reader = newReader(json);
|
JsonReader reader = newReader(json);
|
||||||
for (int i = 0; i < 31; i++) {
|
for (int i = 0; i < 255; i++) {
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
assertThat(reader.nextName()).isEqualTo("a");
|
assertThat(reader.nextName()).isEqualTo("a");
|
||||||
}
|
}
|
||||||
@@ -1054,8 +1052,7 @@ public final class JsonUtf8ReaderTest {
|
|||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
fail();
|
fail();
|
||||||
} catch (JsonDataException expected) {
|
} catch (JsonDataException expected) {
|
||||||
assertThat(expected).hasMessage(
|
assertThat(expected).hasMessage("Nesting too deep at $" + repeat(".a", 255));
|
||||||
"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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,6 +25,7 @@ import org.junit.runners.Parameterized;
|
|||||||
import org.junit.runners.Parameterized.Parameter;
|
import org.junit.runners.Parameterized.Parameter;
|
||||||
import org.junit.runners.Parameterized.Parameters;
|
import org.junit.runners.Parameterized.Parameters;
|
||||||
|
|
||||||
|
import static com.squareup.moshi.TestUtil.repeat;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.junit.Assume.assumeTrue;
|
import static org.junit.Assume.assumeTrue;
|
||||||
@@ -434,15 +435,15 @@ public final class JsonWriterTest {
|
|||||||
|
|
||||||
@Test public void tooDeepNestingArrays() throws IOException {
|
@Test public void tooDeepNestingArrays() throws IOException {
|
||||||
JsonWriter writer = factory.newWriter();
|
JsonWriter writer = factory.newWriter();
|
||||||
for (int i = 0; i < 31; i++) {
|
for (int i = 0; i < 255; i++) {
|
||||||
writer.beginArray();
|
writer.beginArray();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
writer.beginArray();
|
writer.beginArray();
|
||||||
fail();
|
fail();
|
||||||
} catch (JsonDataException expected) {
|
} catch (JsonDataException expected) {
|
||||||
assertThat(expected).hasMessage("Nesting too deep at $[0][0][0][0][0][0][0][0][0][0][0][0][0]"
|
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]: circular reference?");
|
+ repeat("[0]", 255) + ": circular reference?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +465,7 @@ public final class JsonWriterTest {
|
|||||||
|
|
||||||
@Test public void tooDeepNestingObjects() throws IOException {
|
@Test public void tooDeepNestingObjects() throws IOException {
|
||||||
JsonWriter writer = factory.newWriter();
|
JsonWriter writer = factory.newWriter();
|
||||||
for (int i = 0; i < 31; i++) {
|
for (int i = 0; i < 255; i++) {
|
||||||
writer.beginObject();
|
writer.beginObject();
|
||||||
writer.name("a");
|
writer.name("a");
|
||||||
}
|
}
|
||||||
@@ -472,8 +473,8 @@ public final class JsonWriterTest {
|
|||||||
writer.beginObject();
|
writer.beginObject();
|
||||||
fail();
|
fail();
|
||||||
} catch (JsonDataException expected) {
|
} 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."
|
assertThat(expected).hasMessage("Nesting too deep at $"
|
||||||
+ "a.a.a.a.a.a.a.a.a.a.a.a: circular reference?");
|
+ repeat(".a", 255) + ": circular reference?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -38,6 +38,7 @@ import okio.Buffer;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static com.squareup.moshi.TestUtil.newReader;
|
import static com.squareup.moshi.TestUtil.newReader;
|
||||||
|
import static com.squareup.moshi.TestUtil.repeat;
|
||||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
@@ -971,8 +972,8 @@ public final class MoshiTest {
|
|||||||
moshi.adapter(Object.class).toJson(map);
|
moshi.adapter(Object.class).toJson(map);
|
||||||
fail();
|
fail();
|
||||||
} catch (JsonDataException expected) {
|
} 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."
|
assertThat(expected).hasMessage("Nesting too deep at $"
|
||||||
+ "a.a.a.a.a.a.a.a.a.a.a.a: circular reference?");
|
+ repeat(".a", 255) + ": circular reference?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,8 +985,8 @@ public final class MoshiTest {
|
|||||||
moshi.adapter(Object.class).toJson(list);
|
moshi.adapter(Object.class).toJson(list);
|
||||||
fail();
|
fail();
|
||||||
} catch (JsonDataException expected) {
|
} catch (JsonDataException expected) {
|
||||||
assertThat(expected).hasMessage("Nesting too deep at $[0][0][0][0][0][0][0][0][0][0][0][0][0]"
|
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]: circular reference?");
|
+ repeat("[0]", 255) + ": circular reference?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -999,8 +1000,8 @@ public final class MoshiTest {
|
|||||||
moshi.adapter(Object.class).toJson(list);
|
moshi.adapter(Object.class).toJson(list);
|
||||||
fail();
|
fail();
|
||||||
} catch (JsonDataException expected) {
|
} 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]."
|
assertThat(expected).hasMessage("Nesting too deep at $[0]"
|
||||||
+ "a[0].a[0].a[0].a[0].a[0].a[0].a[0].a[0]: circular reference?");
|
+ repeat(".a[0]", 127) + ": circular reference?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -30,6 +30,14 @@ final class TestUtil {
|
|||||||
return new String(array);
|
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() {
|
private TestUtil() {
|
||||||
throw new AssertionError("No instances.");
|
throw new AssertionError("No instances.");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user