mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
Limit to 31 levels of nested structure.
With the implicit enclosing document this is 32 levels total. The limit is arbitrary but sufficient - deeper limits yield code that fails with StackOverflowExceptions during the depth-first traversal. We were previously broken in JsonWriter on this - the code we added to support path building was accessing the top of the stack before the stack had been resized, causing a crash. Because this crash has existed forever without much outcry we know the limit is likely sufficient for our existing users. Closes: https://github.com/square/moshi/issues/189
This commit is contained in:
@@ -57,14 +57,17 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
|||||||
/** The output data, containing at most one top-level array or object. */
|
/** The output data, containing at most one top-level array or object. */
|
||||||
private final BufferedSink sink;
|
private final BufferedSink sink;
|
||||||
|
|
||||||
private int[] stack = new int[32];
|
// 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.
|
||||||
|
private final int[] stack = new int[32];
|
||||||
private int stackSize = 0;
|
private int stackSize = 0;
|
||||||
{
|
{
|
||||||
push(EMPTY_DOCUMENT);
|
push(EMPTY_DOCUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] pathNames = new String[32];
|
private final String[] pathNames = new String[32];
|
||||||
private int[] pathIndices = new int[32];
|
private final int[] pathIndices = new int[32];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A string containing a full set of spaces for a single level of
|
* A string containing a full set of spaces for a single level of
|
||||||
@@ -143,8 +146,8 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
|||||||
*/
|
*/
|
||||||
private JsonWriter open(int empty, String openBracket) throws IOException {
|
private JsonWriter open(int empty, String openBracket) throws IOException {
|
||||||
beforeValue();
|
beforeValue();
|
||||||
pathIndices[stackSize] = 0;
|
|
||||||
push(empty);
|
push(empty);
|
||||||
|
pathIndices[stackSize - 1] = 0;
|
||||||
sink.writeUtf8(openBracket);
|
sink.writeUtf8(openBracket);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -175,9 +178,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
|||||||
|
|
||||||
private void push(int newTop) {
|
private void push(int newTop) {
|
||||||
if (stackSize == stack.length) {
|
if (stackSize == stack.length) {
|
||||||
int[] newStack = new int[stackSize * 2];
|
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
|
||||||
System.arraycopy(stack, 0, newStack, 0, stackSize);
|
|
||||||
stack = newStack;
|
|
||||||
}
|
}
|
||||||
stack[stackSize++] = newTop;
|
stack[stackSize++] = newTop;
|
||||||
}
|
}
|
||||||
|
@@ -92,17 +92,17 @@ final class BufferedSourceJsonReader extends JsonReader {
|
|||||||
*/
|
*/
|
||||||
private String peekedString;
|
private String peekedString;
|
||||||
|
|
||||||
/*
|
// 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%.
|
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
|
||||||
*/
|
// StackOverflowErrors.
|
||||||
private int[] stack = new int[32];
|
private final int[] stack = new int[32];
|
||||||
private int stackSize = 0;
|
private int stackSize = 0;
|
||||||
{
|
{
|
||||||
stack[stackSize++] = JsonScope.EMPTY_DOCUMENT;
|
stack[stackSize++] = JsonScope.EMPTY_DOCUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] pathNames = new String[32];
|
private final String[] pathNames = new String[32];
|
||||||
private int[] pathIndices = new int[32];
|
private final int[] pathIndices = new int[32];
|
||||||
|
|
||||||
BufferedSourceJsonReader(BufferedSource source) {
|
BufferedSourceJsonReader(BufferedSource source) {
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
@@ -901,15 +901,7 @@ final class BufferedSourceJsonReader extends JsonReader {
|
|||||||
|
|
||||||
private void push(int newTop) {
|
private void push(int newTop) {
|
||||||
if (stackSize == stack.length) {
|
if (stackSize == stack.length) {
|
||||||
int[] newStack = new int[stackSize * 2];
|
throw new JsonDataException("Nesting too deep at " + getPath());
|
||||||
int[] newPathIndices = new int[stackSize * 2];
|
|
||||||
String[] newPathNames = new String[stackSize * 2];
|
|
||||||
System.arraycopy(stack, 0, newStack, 0, stackSize);
|
|
||||||
System.arraycopy(pathIndices, 0, newPathIndices, 0, stackSize);
|
|
||||||
System.arraycopy(pathNames, 0, newPathNames, 0, stackSize);
|
|
||||||
stack = newStack;
|
|
||||||
pathIndices = newPathIndices;
|
|
||||||
pathNames = newPathNames;
|
|
||||||
}
|
}
|
||||||
stack[stackSize++] = newTop;
|
stack[stackSize++] = newTop;
|
||||||
}
|
}
|
||||||
|
@@ -22,6 +22,10 @@ package com.squareup.moshi;
|
|||||||
*
|
*
|
||||||
* <p>Exceptions of this type should be fixed by either changing the application code to accept
|
* <p>Exceptions of this type should be fixed by either changing the application code to accept
|
||||||
* the unexpected JSON, or by changing the JSON to conform to the application's expectations.
|
* the unexpected JSON, or by changing the JSON to conform to the application's expectations.
|
||||||
|
*
|
||||||
|
* <p>This exception may also be triggered if a document's nesting exceeds 31 levels. This depth is
|
||||||
|
* sufficient for all practical applications, but shallow enough to avoid uglier failures like
|
||||||
|
* {@link StackOverflowError}.
|
||||||
*/
|
*/
|
||||||
public final class JsonDataException extends RuntimeException {
|
public final class JsonDataException extends RuntimeException {
|
||||||
public JsonDataException() {
|
public JsonDataException() {
|
||||||
|
@@ -50,7 +50,7 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
|||||||
writer.beginObject();
|
writer.beginObject();
|
||||||
for (Map.Entry<K, V> entry : map.entrySet()) {
|
for (Map.Entry<K, V> entry : map.entrySet()) {
|
||||||
if (entry.getKey() == null) {
|
if (entry.getKey() == null) {
|
||||||
throw new JsonDataException("Map key is null at path " + writer.getPath());
|
throw new JsonDataException("Map key is null at " + writer.getPath());
|
||||||
}
|
}
|
||||||
writer.promoteNameToValue();
|
writer.promoteNameToValue();
|
||||||
keyAdapter.toJson(writer, entry.getKey());
|
keyAdapter.toJson(writer, entry.getKey());
|
||||||
|
@@ -419,31 +419,62 @@ public final class BufferedSinkJsonWriterTest {
|
|||||||
@Test public void deepNestingArrays() throws IOException {
|
@Test public void deepNestingArrays() throws IOException {
|
||||||
Buffer buffer = new Buffer();
|
Buffer buffer = new Buffer();
|
||||||
JsonWriter jsonWriter = JsonWriter.of(buffer);
|
JsonWriter jsonWriter = JsonWriter.of(buffer);
|
||||||
for (int i = 0; i < 20; i++) {
|
for (int i = 0; i < 31; i++) {
|
||||||
jsonWriter.beginArray();
|
jsonWriter.beginArray();
|
||||||
}
|
}
|
||||||
for (int i = 0; i < 20; i++) {
|
for (int i = 0; i < 31; i++) {
|
||||||
jsonWriter.endArray();
|
jsonWriter.endArray();
|
||||||
}
|
}
|
||||||
assertThat(buffer.readUtf8()).isEqualTo("[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]");
|
assertThat(buffer.readUtf8())
|
||||||
|
.isEqualTo("[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void tooDeepNestingArrays() throws IOException {
|
||||||
|
Buffer buffer = new Buffer();
|
||||||
|
JsonWriter jsonWriter = JsonWriter.of(buffer);
|
||||||
|
for (int i = 0; i < 31; i++) {
|
||||||
|
jsonWriter.beginArray();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
jsonWriter.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?");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void deepNestingObjects() throws IOException {
|
@Test public void deepNestingObjects() throws IOException {
|
||||||
Buffer buffer = new Buffer();
|
Buffer buffer = new Buffer();
|
||||||
JsonWriter jsonWriter = JsonWriter.of(buffer);
|
JsonWriter jsonWriter = JsonWriter.of(buffer);
|
||||||
|
for (int i = 0; i < 31; i++) {
|
||||||
jsonWriter.beginObject();
|
jsonWriter.beginObject();
|
||||||
for (int i = 0; i < 20; i++) {
|
|
||||||
jsonWriter.name("a");
|
jsonWriter.name("a");
|
||||||
|
}
|
||||||
|
jsonWriter.value(true);
|
||||||
|
for (int i = 0; i < 31; i++) {
|
||||||
|
jsonWriter.endObject();
|
||||||
|
}
|
||||||
|
assertThat(buffer.readUtf8()).isEqualTo(""
|
||||||
|
+ "{\"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\":true}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void tooDeepNestingObjects() throws IOException {
|
||||||
|
Buffer buffer = new Buffer();
|
||||||
|
JsonWriter jsonWriter = JsonWriter.of(buffer);
|
||||||
|
for (int i = 0; i < 31; i++) {
|
||||||
jsonWriter.beginObject();
|
jsonWriter.beginObject();
|
||||||
|
jsonWriter.name("a");
|
||||||
}
|
}
|
||||||
for (int i = 0; i < 20; i++) {
|
try {
|
||||||
jsonWriter.endObject();
|
jsonWriter.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?");
|
||||||
}
|
}
|
||||||
jsonWriter.endObject();
|
|
||||||
assertThat(buffer.readUtf8()).isEqualTo(
|
|
||||||
"{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":"
|
|
||||||
+ "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{"
|
|
||||||
+ "}}}}}}}}}}}}}}}}}}}}}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void repeatedName() throws IOException {
|
@Test public void repeatedName() throws IOException {
|
||||||
|
@@ -1498,43 +1498,77 @@ public final class BufferedSourceJsonReaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test public void deeplyNestedArrays() throws IOException {
|
@Test public void deeplyNestedArrays() throws IOException {
|
||||||
// this is nested 40 levels deep; Gson is tuned for nesting is 30 levels deep or fewer
|
JsonReader reader = newReader("[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]");
|
||||||
JsonReader reader = newReader(
|
for (int i = 0; i < 31; i++) {
|
||||||
"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]");
|
|
||||||
for (int i = 0; i < 40; i++) {
|
|
||||||
reader.beginArray();
|
reader.beginArray();
|
||||||
}
|
}
|
||||||
assertThat(reader.getPath()).isEqualTo(
|
assertThat(reader.getPath()).isEqualTo("$[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][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][0][0][0][0][0][0][0][0][0]");
|
for (int i = 0; i < 31; i++) {
|
||||||
for (int i = 0; i < 40; i++) {
|
|
||||||
reader.endArray();
|
reader.endArray();
|
||||||
}
|
}
|
||||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test public void tooDeeplyNestedArrays() throws IOException {
|
||||||
|
JsonReader reader = newReader(
|
||||||
|
"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]");
|
||||||
|
for (int i = 0; i < 31; 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]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test public void deeplyNestedObjects() throws IOException {
|
@Test public void deeplyNestedObjects() throws IOException {
|
||||||
// Build a JSON document structured like {"a":{"a":{"a":{"a":true}}}}, but 40 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 < 40; i++) {
|
for (int i = 0; i < 31; i++) {
|
||||||
json = String.format(array, json);
|
json = String.format(array, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonReader reader = newReader(json);
|
JsonReader reader = newReader(json);
|
||||||
for (int i = 0; i < 40; i++) {
|
for (int i = 0; i < 31; i++) {
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
assertThat(reader.nextName()).isEqualTo("a");
|
assertThat(reader.nextName()).isEqualTo("a");
|
||||||
}
|
}
|
||||||
assertThat(reader.getPath()).isEqualTo("$.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a"
|
assertThat(reader.getPath())
|
||||||
+ ".a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a");
|
.isEqualTo("$.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(reader.nextBoolean()).isTrue();
|
assertThat(reader.nextBoolean()).isTrue();
|
||||||
for (int i = 0; i < 40; i++) {
|
for (int i = 0; i < 31; i++) {
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
}
|
}
|
||||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test public void tooDeeplyNestedObjects() throws IOException {
|
||||||
|
// 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++) {
|
||||||
|
json = String.format(array, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonReader reader = newReader(json);
|
||||||
|
for (int i = 0; i < 31; i++) {
|
||||||
|
reader.beginObject();
|
||||||
|
assertThat(reader.nextName()).isEqualTo("a");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// http://code.google.com/p/google-gson/issues/detail?id=409
|
// http://code.google.com/p/google-gson/issues/detail?id=409
|
||||||
@Test public void stringEndingInSlash() throws IOException {
|
@Test public void stringEndingInSlash() throws IOException {
|
||||||
JsonReader reader = newReader("/");
|
JsonReader reader = newReader("/");
|
||||||
|
@@ -23,7 +23,6 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import okio.Buffer;
|
import okio.Buffer;
|
||||||
import org.assertj.core.data.MapEntry;
|
import org.assertj.core.data.MapEntry;
|
||||||
import org.junit.Ignore;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static com.squareup.moshi.TestUtil.newReader;
|
import static com.squareup.moshi.TestUtil.newReader;
|
||||||
@@ -57,7 +56,7 @@ public final class MapJsonAdapterTest {
|
|||||||
toJson(String.class, Boolean.class, map);
|
toJson(String.class, Boolean.class, map);
|
||||||
fail();
|
fail();
|
||||||
} catch (JsonDataException expected) {
|
} catch (JsonDataException expected) {
|
||||||
assertThat(expected).hasMessage("Map key is null at path $.");
|
assertThat(expected).hasMessage("Map key is null at $.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,13 +23,17 @@ import java.lang.annotation.Retention;
|
|||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.crypto.KeyGenerator;
|
import javax.crypto.KeyGenerator;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static com.squareup.moshi.TestUtil.newReader;
|
import static com.squareup.moshi.TestUtil.newReader;
|
||||||
@@ -816,6 +820,47 @@ public final class MoshiTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test public void referenceCyclesOnArrays() throws Exception {
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
map.put("a", map);
|
||||||
|
try {
|
||||||
|
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?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void referenceCyclesOnObjects() throws Exception {
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
List<Object> list = new ArrayList<>();
|
||||||
|
list.add(list);
|
||||||
|
try {
|
||||||
|
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?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test public void referenceCyclesOnMixedTypes() throws Exception {
|
||||||
|
Moshi moshi = new Moshi.Builder().build();
|
||||||
|
List<Object> list = new ArrayList<>();
|
||||||
|
Map<String, Object> map = new LinkedHashMap<>();
|
||||||
|
list.add(map);
|
||||||
|
map.put("a", list);
|
||||||
|
try {
|
||||||
|
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?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static class Pizza {
|
static class Pizza {
|
||||||
final int diameter;
|
final int diameter;
|
||||||
final boolean extraCheese;
|
final boolean extraCheese;
|
||||||
|
Reference in New Issue
Block a user