diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSinkJsonWriter.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSinkJsonWriter.kt index d6aa2e5b635..347201e939b 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSinkJsonWriter.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSinkJsonWriter.kt @@ -16,9 +16,8 @@ package com.apollographql.apollo3.api.json import com.apollographql.apollo3.api.Upload -import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.MAX_STACK_SIZE +import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.INITIAL_STACK_SIZE import com.apollographql.apollo3.api.json.internal.JsonScope -import com.apollographql.apollo3.exception.JsonDataException import okio.BufferedSink import okio.IOException import kotlin.jvm.JvmOverloads @@ -39,12 +38,10 @@ class BufferedSinkJsonWriter @JvmOverloads constructor( private val sink: BufferedSink, private val indent: String? = null, ) : JsonWriter { - // The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits up to MAX_STACK_SIZE levels of nesting including - // the top-level document. Deeper nesting is prone to trigger StackOverflowErrors. private var stackSize = 0 - private val scopes = IntArray(MAX_STACK_SIZE) - private val pathNames = arrayOfNulls(MAX_STACK_SIZE) - private val pathIndices = IntArray(MAX_STACK_SIZE) + private var scopes = IntArray(INITIAL_STACK_SIZE) + private var pathNames = arrayOfNulls(INITIAL_STACK_SIZE) + private var pathIndices = IntArray(INITIAL_STACK_SIZE) /** The name/value separator; either ":" or ": ". */ private val separator: String @@ -256,7 +253,9 @@ class BufferedSinkJsonWriter @JvmOverloads constructor( private fun pushScope(newTop: Int) { if (stackSize == scopes.size) { - throw JsonDataException("Nesting too deep at $path: circular reference?") + scopes = scopes.copyOf(scopes.size * 2) + pathNames = pathNames.copyOf(pathNames.size * 2) + pathIndices = pathIndices.copyOf(pathIndices.size * 2) } scopes[stackSize++] = newTop } diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSourceJsonReader.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSourceJsonReader.kt index 91ef4568022..c21e86a2e6f 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSourceJsonReader.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSourceJsonReader.kt @@ -52,19 +52,14 @@ class BufferedSourceJsonReader(private val source: BufferedSource) : JsonReader */ private var peekedString: String? = null - /** - * The nesting stack. Using a manual array rather than an ArrayList saves 20%. - * This stack permits up to MAX_STACK_SIZE levels of nesting including the top-level document. - * Deeper nesting is prone to trigger StackOverflowErrors. - */ - private val stack = IntArray(MAX_STACK_SIZE).apply { + private var stack = IntArray(INITIAL_STACK_SIZE).apply { this[0] = JsonScope.EMPTY_DOCUMENT } private var stackSize = 1 - private val pathNames = arrayOfNulls(MAX_STACK_SIZE) - private val pathIndices = IntArray(MAX_STACK_SIZE) + private var pathNames = arrayOfNulls(INITIAL_STACK_SIZE) + private var pathIndices = IntArray(INITIAL_STACK_SIZE) - private val indexStack = IntArray(MAX_STACK_SIZE).apply { + private var indexStack = IntArray(INITIAL_STACK_SIZE).apply { this[0] = 0 } private var indexStackSize = 1 @@ -746,7 +741,12 @@ class BufferedSourceJsonReader(private val source: BufferedSource) : JsonReader } private fun push(newTop: Int) { - if (stackSize == stack.size) throw JsonDataException("Nesting too deep at " + getPath()) + if (stackSize == stack.size) { + stack = stack.copyOf(stack.size * 2) + pathNames = pathNames.copyOf(pathNames.size * 2) + pathIndices = pathIndices.copyOf(pathIndices.size * 2) + indexStack = indexStack.copyOf(indexStack.size * 2) + } stack[stackSize++] = newTop } @@ -888,6 +888,6 @@ class BufferedSourceJsonReader(private val source: BufferedSource) : JsonReader private const val NUMBER_CHAR_EXP_SIGN = 6 private const val NUMBER_CHAR_EXP_DIGIT = 7 - internal const val MAX_STACK_SIZE = 256 + internal const val INITIAL_STACK_SIZE = 64 } } diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/MapJsonReader.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/MapJsonReader.kt index b63f45a2f6e..f5f00e38f23 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/MapJsonReader.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/MapJsonReader.kt @@ -1,6 +1,6 @@ package com.apollographql.apollo3.api.json -import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.MAX_STACK_SIZE +import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.INITIAL_STACK_SIZE import com.apollographql.apollo3.api.json.MapJsonReader.Companion.buffer import com.apollographql.apollo3.api.json.internal.toDoubleExact import com.apollographql.apollo3.api.json.internal.toIntExact @@ -50,14 +50,14 @@ constructor( * - a String representing the current key to be read in a Map * - null if peekedToken is BEGIN_OBJECT */ - private val path = arrayOfNulls(MAX_STACK_SIZE) + private var path = arrayOfNulls(INITIAL_STACK_SIZE) /** * The current object memorized in case we need to rewind */ - private var containerStack = arrayOfNulls>(MAX_STACK_SIZE) - private val iteratorStack = arrayOfNulls>(MAX_STACK_SIZE) - private val nameIndexStack = IntArray(MAX_STACK_SIZE) + private var containerStack = arrayOfNulls>(INITIAL_STACK_SIZE) + private var iteratorStack = arrayOfNulls>(INITIAL_STACK_SIZE) + private var nameIndexStack = IntArray(INITIAL_STACK_SIZE) private var stackSize = 0 @@ -113,6 +113,16 @@ constructor( } } + private fun increaseStack() { + if (stackSize == path.size) { + path = path.copyOf(path.size * 2) + containerStack = containerStack.copyOf(containerStack.size * 2) + nameIndexStack = nameIndexStack.copyOf(nameIndexStack.size * 2) + iteratorStack = iteratorStack.copyOf(iteratorStack.size * 2) + } + stackSize++ + } + override fun beginArray() = apply { if (peek() != JsonReader.Token.BEGIN_ARRAY) { throw JsonDataException("Expected BEGIN_ARRAY but was ${peek()} at path ${getPathAsString()}") @@ -120,10 +130,7 @@ constructor( val currentValue = peekedData as List - check(stackSize < MAX_STACK_SIZE) { - "Nesting too deep" - } - stackSize++ + increaseStack() path[stackSize - 1] = -1 iteratorStack[stackSize - 1] = currentValue.iterator() @@ -145,10 +152,8 @@ constructor( throw JsonDataException("Expected BEGIN_OBJECT but was ${peek()} at path ${getPathAsString()}") } - check(stackSize < MAX_STACK_SIZE) { - "Nesting too deep" - } - stackSize++ + increaseStack() + @Suppress("UNCHECKED_CAST") containerStack[stackSize - 1] = peekedData as Map diff --git a/libraries/apollo-api/src/commonTest/kotlin/test/JsonTest.kt b/libraries/apollo-api/src/commonTest/kotlin/test/JsonTest.kt index 41a6202ecf4..10d697358a6 100644 --- a/libraries/apollo-api/src/commonTest/kotlin/test/JsonTest.kt +++ b/libraries/apollo-api/src/commonTest/kotlin/test/JsonTest.kt @@ -3,8 +3,12 @@ package test import com.apollographql.apollo3.api.AnyAdapter import com.apollographql.apollo3.api.CustomScalarAdapters import com.apollographql.apollo3.api.LongAdapter +import com.apollographql.apollo3.api.json.MapJsonReader import com.apollographql.apollo3.api.json.MapJsonWriter import com.apollographql.apollo3.api.json.buildJsonString +import com.apollographql.apollo3.api.json.jsonReader +import com.apollographql.apollo3.api.json.readAny +import okio.Buffer import kotlin.test.Test import kotlin.test.assertEquals @@ -27,4 +31,36 @@ class JsonTest { } assertEquals("9223372036854775807", json) } + + @Test + fun canReadAndWriteVeryDeeplyNestedJsonSource() { + val json = buildJsonString { + val nesting = 1025 + repeat(nesting) { + beginObject() + name("child") + } + value("yooooo") + repeat(nesting) { + endObject() + } + } + + Buffer().writeUtf8(json).jsonReader().readAny() + } + + @Test + fun canReadVeryDeeplyNestedJsonMap() { + val root = mutableMapOf() + var map = root + val nesting = 1025 + + repeat(nesting) { + val newMap = mutableMapOf() + map.put("child", newMap) + map = newMap + } + + MapJsonReader(root).readAny() + } } diff --git a/libraries/apollo-api/src/jsMain/kotlin/com/apollographql/apollo3/api/json/DynamicJsJsonReader.kt b/libraries/apollo-api/src/jsMain/kotlin/com/apollographql/apollo3/api/json/DynamicJsJsonReader.kt index da6c850c81d..b138c5b0cd4 100644 --- a/libraries/apollo-api/src/jsMain/kotlin/com/apollographql/apollo3/api/json/DynamicJsJsonReader.kt +++ b/libraries/apollo-api/src/jsMain/kotlin/com/apollographql/apollo3/api/json/DynamicJsJsonReader.kt @@ -1,6 +1,6 @@ package com.apollographql.apollo3.api.json -import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.MAX_STACK_SIZE +import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.INITIAL_STACK_SIZE import com.apollographql.apollo3.api.json.MapJsonReader.Companion.buffer import com.apollographql.apollo3.api.json.internal.toDoubleExact import com.apollographql.apollo3.api.json.internal.toIntExact @@ -74,14 +74,14 @@ constructor( * - a String representing the current key to be read in a Map * - null if peekedToken is BEGIN_OBJECT */ - private val path = arrayOfNulls(MAX_STACK_SIZE) + private var path = arrayOfNulls(INITIAL_STACK_SIZE) /** * The current object memorized in case we need to rewind */ - private var containerStack = arrayOfNulls(MAX_STACK_SIZE) - private val iteratorStack = arrayOfNulls(MAX_STACK_SIZE) - private val nameIndexStack = IntArray(MAX_STACK_SIZE) + private var containerStack = arrayOfNulls(INITIAL_STACK_SIZE) + private var iteratorStack = arrayOfNulls(INITIAL_STACK_SIZE) + private var nameIndexStack = IntArray(INITIAL_STACK_SIZE) private var stackSize = 0 @@ -143,17 +143,25 @@ constructor( } } + private fun increaseStack() { + if (stackSize == path.size) { + path = path.copyOf(path.size * 2) + containerStack = containerStack.copyOf(containerStack.size * 2) + nameIndexStack = nameIndexStack.copyOf(nameIndexStack.size * 2) + iteratorStack = iteratorStack.copyOf(iteratorStack.size * 2) + } + stackSize++ + } + + override fun beginArray() = apply { if (peek() != JsonReader.Token.BEGIN_ARRAY) { throw JsonDataException("Expected BEGIN_ARRAY but was ${peek()} at path ${getPathAsString()}") } val currentValue = peekedData as Array<*> - - check(stackSize < MAX_STACK_SIZE) { - "Nesting too deep" - } - stackSize++ + + increaseStack() path[stackSize - 1] = -1 iteratorStack[stackSize - 1] = IteratorWrapper.StandardIterator(currentValue.iterator()) @@ -175,10 +183,7 @@ constructor( throw JsonDataException("Expected BEGIN_OBJECT but was ${peek()} at path ${getPathAsString()}") } - check(stackSize < MAX_STACK_SIZE) { - "Nesting too deep" - } - stackSize++ + increaseStack() containerStack[stackSize - 1] = peekedData.asDynamic() rewind()