Skip to content

Commit 3972d9c

Browse files
authored
Remove limitation on the JSON nesting. If the JSON is way too nested, an OutOfMemory exception will happen (#5161)
1 parent 645d061 commit 3972d9c

File tree

5 files changed

+91
-46
lines changed

5 files changed

+91
-46
lines changed

libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSinkJsonWriter.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616
package com.apollographql.apollo3.api.json
1717

1818
import com.apollographql.apollo3.api.Upload
19-
import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.MAX_STACK_SIZE
19+
import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.INITIAL_STACK_SIZE
2020
import com.apollographql.apollo3.api.json.internal.JsonScope
21-
import com.apollographql.apollo3.exception.JsonDataException
2221
import okio.BufferedSink
2322
import okio.IOException
2423
import kotlin.jvm.JvmOverloads
@@ -39,12 +38,10 @@ class BufferedSinkJsonWriter @JvmOverloads constructor(
3938
private val sink: BufferedSink,
4039
private val indent: String? = null,
4140
) : JsonWriter {
42-
// 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
43-
// the top-level document. Deeper nesting is prone to trigger StackOverflowErrors.
4441
private var stackSize = 0
45-
private val scopes = IntArray(MAX_STACK_SIZE)
46-
private val pathNames = arrayOfNulls<String>(MAX_STACK_SIZE)
47-
private val pathIndices = IntArray(MAX_STACK_SIZE)
42+
private var scopes = IntArray(INITIAL_STACK_SIZE)
43+
private var pathNames = arrayOfNulls<String>(INITIAL_STACK_SIZE)
44+
private var pathIndices = IntArray(INITIAL_STACK_SIZE)
4845

4946
/** The name/value separator; either ":" or ": ". */
5047
private val separator: String
@@ -256,7 +253,9 @@ class BufferedSinkJsonWriter @JvmOverloads constructor(
256253

257254
private fun pushScope(newTop: Int) {
258255
if (stackSize == scopes.size) {
259-
throw JsonDataException("Nesting too deep at $path: circular reference?")
256+
scopes = scopes.copyOf(scopes.size * 2)
257+
pathNames = pathNames.copyOf(pathNames.size * 2)
258+
pathIndices = pathIndices.copyOf(pathIndices.size * 2)
260259
}
261260
scopes[stackSize++] = newTop
262261
}

libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/BufferedSourceJsonReader.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,14 @@ class BufferedSourceJsonReader(private val source: BufferedSource) : JsonReader
5252
*/
5353
private var peekedString: String? = null
5454

55-
/**
56-
* The nesting stack. Using a manual array rather than an ArrayList saves 20%.
57-
* This stack permits up to MAX_STACK_SIZE levels of nesting including the top-level document.
58-
* Deeper nesting is prone to trigger StackOverflowErrors.
59-
*/
60-
private val stack = IntArray(MAX_STACK_SIZE).apply {
55+
private var stack = IntArray(INITIAL_STACK_SIZE).apply {
6156
this[0] = JsonScope.EMPTY_DOCUMENT
6257
}
6358
private var stackSize = 1
64-
private val pathNames = arrayOfNulls<String>(MAX_STACK_SIZE)
65-
private val pathIndices = IntArray(MAX_STACK_SIZE)
59+
private var pathNames = arrayOfNulls<String>(INITIAL_STACK_SIZE)
60+
private var pathIndices = IntArray(INITIAL_STACK_SIZE)
6661

67-
private val indexStack = IntArray(MAX_STACK_SIZE).apply {
62+
private var indexStack = IntArray(INITIAL_STACK_SIZE).apply {
6863
this[0] = 0
6964
}
7065
private var indexStackSize = 1
@@ -746,7 +741,12 @@ class BufferedSourceJsonReader(private val source: BufferedSource) : JsonReader
746741
}
747742

748743
private fun push(newTop: Int) {
749-
if (stackSize == stack.size) throw JsonDataException("Nesting too deep at " + getPath())
744+
if (stackSize == stack.size) {
745+
stack = stack.copyOf(stack.size * 2)
746+
pathNames = pathNames.copyOf(pathNames.size * 2)
747+
pathIndices = pathIndices.copyOf(pathIndices.size * 2)
748+
indexStack = indexStack.copyOf(indexStack.size * 2)
749+
}
750750
stack[stackSize++] = newTop
751751
}
752752

@@ -888,6 +888,6 @@ class BufferedSourceJsonReader(private val source: BufferedSource) : JsonReader
888888
private const val NUMBER_CHAR_EXP_SIGN = 6
889889
private const val NUMBER_CHAR_EXP_DIGIT = 7
890890

891-
internal const val MAX_STACK_SIZE = 256
891+
internal const val INITIAL_STACK_SIZE = 64
892892
}
893893
}

libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/json/MapJsonReader.kt

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.apollographql.apollo3.api.json
22

3-
import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.MAX_STACK_SIZE
3+
import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.INITIAL_STACK_SIZE
44
import com.apollographql.apollo3.api.json.MapJsonReader.Companion.buffer
55
import com.apollographql.apollo3.api.json.internal.toDoubleExact
66
import com.apollographql.apollo3.api.json.internal.toIntExact
@@ -50,14 +50,14 @@ constructor(
5050
* - a String representing the current key to be read in a Map
5151
* - null if peekedToken is BEGIN_OBJECT
5252
*/
53-
private val path = arrayOfNulls<Any>(MAX_STACK_SIZE)
53+
private var path = arrayOfNulls<Any>(INITIAL_STACK_SIZE)
5454

5555
/**
5656
* The current object memorized in case we need to rewind
5757
*/
58-
private var containerStack = arrayOfNulls<Map<String, Any?>>(MAX_STACK_SIZE)
59-
private val iteratorStack = arrayOfNulls<Iterator<*>>(MAX_STACK_SIZE)
60-
private val nameIndexStack = IntArray(MAX_STACK_SIZE)
58+
private var containerStack = arrayOfNulls<Map<String, Any?>>(INITIAL_STACK_SIZE)
59+
private var iteratorStack = arrayOfNulls<Iterator<*>>(INITIAL_STACK_SIZE)
60+
private var nameIndexStack = IntArray(INITIAL_STACK_SIZE)
6161

6262
private var stackSize = 0
6363

@@ -113,17 +113,24 @@ constructor(
113113
}
114114
}
115115

116+
private fun increaseStack() {
117+
if (stackSize == path.size) {
118+
path = path.copyOf(path.size * 2)
119+
containerStack = containerStack.copyOf(containerStack.size * 2)
120+
nameIndexStack = nameIndexStack.copyOf(nameIndexStack.size * 2)
121+
iteratorStack = iteratorStack.copyOf(iteratorStack.size * 2)
122+
}
123+
stackSize++
124+
}
125+
116126
override fun beginArray() = apply {
117127
if (peek() != JsonReader.Token.BEGIN_ARRAY) {
118128
throw JsonDataException("Expected BEGIN_ARRAY but was ${peek()} at path ${getPathAsString()}")
119129
}
120130

121131
val currentValue = peekedData as List<Any?>
122132

123-
check(stackSize < MAX_STACK_SIZE) {
124-
"Nesting too deep"
125-
}
126-
stackSize++
133+
increaseStack()
127134

128135
path[stackSize - 1] = -1
129136
iteratorStack[stackSize - 1] = currentValue.iterator()
@@ -145,10 +152,8 @@ constructor(
145152
throw JsonDataException("Expected BEGIN_OBJECT but was ${peek()} at path ${getPathAsString()}")
146153
}
147154

148-
check(stackSize < MAX_STACK_SIZE) {
149-
"Nesting too deep"
150-
}
151-
stackSize++
155+
increaseStack()
156+
152157
@Suppress("UNCHECKED_CAST")
153158
containerStack[stackSize - 1] = peekedData as Map<String, Any?>
154159

libraries/apollo-api/src/commonTest/kotlin/test/JsonTest.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ package test
33
import com.apollographql.apollo3.api.AnyAdapter
44
import com.apollographql.apollo3.api.CustomScalarAdapters
55
import com.apollographql.apollo3.api.LongAdapter
6+
import com.apollographql.apollo3.api.json.MapJsonReader
67
import com.apollographql.apollo3.api.json.MapJsonWriter
78
import com.apollographql.apollo3.api.json.buildJsonString
9+
import com.apollographql.apollo3.api.json.jsonReader
10+
import com.apollographql.apollo3.api.json.readAny
11+
import okio.Buffer
812
import kotlin.test.Test
913
import kotlin.test.assertEquals
1014

@@ -27,4 +31,36 @@ class JsonTest {
2731
}
2832
assertEquals("9223372036854775807", json)
2933
}
34+
35+
@Test
36+
fun canReadAndWriteVeryDeeplyNestedJsonSource() {
37+
val json = buildJsonString {
38+
val nesting = 1025
39+
repeat(nesting) {
40+
beginObject()
41+
name("child")
42+
}
43+
value("yooooo")
44+
repeat(nesting) {
45+
endObject()
46+
}
47+
}
48+
49+
Buffer().writeUtf8(json).jsonReader().readAny()
50+
}
51+
52+
@Test
53+
fun canReadVeryDeeplyNestedJsonMap() {
54+
val root = mutableMapOf<String, Any>()
55+
var map = root
56+
val nesting = 1025
57+
58+
repeat(nesting) {
59+
val newMap = mutableMapOf<String, Any>()
60+
map.put("child", newMap)
61+
map = newMap
62+
}
63+
64+
MapJsonReader(root).readAny()
65+
}
3066
}

libraries/apollo-api/src/jsMain/kotlin/com/apollographql/apollo3/api/json/DynamicJsJsonReader.kt

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.apollographql.apollo3.api.json
22

3-
import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.MAX_STACK_SIZE
3+
import com.apollographql.apollo3.api.json.BufferedSourceJsonReader.Companion.INITIAL_STACK_SIZE
44
import com.apollographql.apollo3.api.json.MapJsonReader.Companion.buffer
55
import com.apollographql.apollo3.api.json.internal.toDoubleExact
66
import com.apollographql.apollo3.api.json.internal.toIntExact
@@ -74,14 +74,14 @@ constructor(
7474
* - a String representing the current key to be read in a Map
7575
* - null if peekedToken is BEGIN_OBJECT
7676
*/
77-
private val path = arrayOfNulls<Any>(MAX_STACK_SIZE)
77+
private var path = arrayOfNulls<Any>(INITIAL_STACK_SIZE)
7878

7979
/**
8080
* The current object memorized in case we need to rewind
8181
*/
82-
private var containerStack = arrayOfNulls<Any?>(MAX_STACK_SIZE)
83-
private val iteratorStack = arrayOfNulls<IteratorWrapper>(MAX_STACK_SIZE)
84-
private val nameIndexStack = IntArray(MAX_STACK_SIZE)
82+
private var containerStack = arrayOfNulls<Any?>(INITIAL_STACK_SIZE)
83+
private var iteratorStack = arrayOfNulls<IteratorWrapper>(INITIAL_STACK_SIZE)
84+
private var nameIndexStack = IntArray(INITIAL_STACK_SIZE)
8585

8686
private var stackSize = 0
8787

@@ -143,17 +143,25 @@ constructor(
143143
}
144144
}
145145

146+
private fun increaseStack() {
147+
if (stackSize == path.size) {
148+
path = path.copyOf(path.size * 2)
149+
containerStack = containerStack.copyOf(containerStack.size * 2)
150+
nameIndexStack = nameIndexStack.copyOf(nameIndexStack.size * 2)
151+
iteratorStack = iteratorStack.copyOf(iteratorStack.size * 2)
152+
}
153+
stackSize++
154+
}
155+
156+
146157
override fun beginArray() = apply {
147158
if (peek() != JsonReader.Token.BEGIN_ARRAY) {
148159
throw JsonDataException("Expected BEGIN_ARRAY but was ${peek()} at path ${getPathAsString()}")
149160
}
150161

151162
val currentValue = peekedData as Array<*>
152-
153-
check(stackSize < MAX_STACK_SIZE) {
154-
"Nesting too deep"
155-
}
156-
stackSize++
163+
164+
increaseStack()
157165

158166
path[stackSize - 1] = -1
159167
iteratorStack[stackSize - 1] = IteratorWrapper.StandardIterator(currentValue.iterator())
@@ -175,10 +183,7 @@ constructor(
175183
throw JsonDataException("Expected BEGIN_OBJECT but was ${peek()} at path ${getPathAsString()}")
176184
}
177185

178-
check(stackSize < MAX_STACK_SIZE) {
179-
"Nesting too deep"
180-
}
181-
stackSize++
186+
increaseStack()
182187
containerStack[stackSize - 1] = peekedData.asDynamic()
183188

184189
rewind()

0 commit comments

Comments
 (0)