Skip to content

Commit a1fa4ae

Browse files
committed
Fix #3447
1 parent ada34dc commit a1fa4ae

File tree

4 files changed

+125
-17
lines changed

4 files changed

+125
-17
lines changed

Diff for: release-notes/CREDITS-2.x

+4
Original file line numberDiff line numberDiff line change
@@ -1452,3 +1452,7 @@ Gary Morgan (morganga@github)
14521452
Jan Judas (kostislav@github)
14531453
* Contributed #3445: Do not strip generic type from `Class<C>` when resolving `JavaType`
14541454
(2.14.0)
1455+
1456+
Deniz Husaj (denizhusaj@github)
1457+
* Reported #3447: Deeply nested JsonNode throws StackOverflowError for toString()
1458+
(2.14.0)

Diff for: release-notes/VERSION-2.x

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Project: jackson-databind
1919
JSON `null` values on reading
2020
#3443: Do not strip generic type from `Class<C>` when resolving `JavaType`
2121
(contributed by Jan J)
22+
#3447: Deeply nested JsonNode throws StackOverflowError for toString()
23+
(reported by Deniz H)
2224
#3476: Implement `JsonNodeFeature.WRITE_NULL_PROPERTIES` to allow skipping
2325
JSON `null` values on writing
2426

Diff for: src/main/java/com/fasterxml/jackson/databind/node/InternalNodeMapper.java

+88-17
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.fasterxml.jackson.databind.node;
22

33
import java.io.IOException;
4+
import java.util.Arrays;
45
import java.util.Iterator;
56
import java.util.Map;
7+
import java.util.NoSuchElementException;
68

79
import com.fasterxml.jackson.core.JsonGenerator;
810

@@ -92,31 +94,100 @@ public void serializeWithType(JsonGenerator g, SerializerProvider ctxt, TypeSeri
9294
serialize(g, ctxt);
9395
}
9496

95-
9697
protected void _serializeNonRecursive(JsonGenerator g, JsonNode node) throws IOException
9798
{
9899
if (node instanceof ObjectNode) {
99-
g.writeStartObject(this);
100-
Iterator<Map.Entry<String, JsonNode>> it = node.fields();
101-
while (it.hasNext()) {
102-
Map.Entry<String, JsonNode> en = it.next();
103-
JsonNode value = en.getValue();
104-
g.writeFieldName(en.getKey());
105-
value.serialize(g, _context);
106-
}
107-
g.writeEndObject();
100+
g.writeStartObject(this, node.size());
101+
_serializeNonRecursive(g, new IteratorStack(), node.fields());
108102
} else if (node instanceof ArrayNode) {
109103
g.writeStartArray(this, node.size());
110-
Iterator<JsonNode> it = node.elements();
111-
while (it.hasNext()) {
112-
// For now, assuming it's either BaseJsonNode, JsonSerializable
113-
JsonNode value = it.next();
114-
value.serialize(g, _context);
115-
}
116-
g.writeEndArray();
104+
_serializeNonRecursive(g, new IteratorStack(), node.elements());
117105
} else {
118106
node.serialize(g, _context);
119107
}
120108
}
109+
110+
protected void _serializeNonRecursive(JsonGenerator g, IteratorStack stack,
111+
final Iterator<?> rootIterator)
112+
throws IOException
113+
{
114+
Iterator<?> currIt = rootIterator;
115+
while (true) {
116+
// First: any more elements from the current iterator?
117+
while (currIt.hasNext()) {
118+
JsonNode value;
119+
120+
// Otherwise we do have another Map or Array element to handle
121+
Object elem = currIt.next();
122+
if (elem instanceof Map.Entry<?,?>) {
123+
@SuppressWarnings("unchecked")
124+
Map.Entry<String, JsonNode> en = (Map.Entry<String, JsonNode>) elem;
125+
g.writeFieldName(en.getKey());
126+
value = en.getValue();
127+
} else {
128+
value = (JsonNode) elem;
129+
}
130+
if (value instanceof ObjectNode) {
131+
stack.push(currIt);
132+
currIt = value.fields();
133+
g.writeStartObject(value, value.size());
134+
} else if (value instanceof ArrayNode) {
135+
stack.push(currIt);
136+
currIt = value.elements();
137+
g.writeStartArray(value, value.size());
138+
} else {
139+
value.serialize(g, _context);
140+
}
141+
}
142+
if (g.getOutputContext().inArray()) {
143+
g.writeEndArray();
144+
} else {
145+
g.writeEndObject();
146+
}
147+
currIt = stack.popOrNull();
148+
if (currIt == null) {
149+
return;
150+
}
151+
}
152+
}
153+
}
154+
155+
/**
156+
* Optimized variant similar in functionality to (a subset of)
157+
* {@link java.util.ArrayDeque}; used to hold enclosing Array/Object
158+
* nodes during recursion-as-iteration.
159+
*/
160+
final static class IteratorStack
161+
{
162+
private Iterator<?>[] _stack;
163+
private int _top, _end;
164+
165+
public IteratorStack() { }
166+
167+
public void push(Iterator<?> it)
168+
{
169+
if (_top < _end) {
170+
_stack[_top++] = it; // lgtm [java/dereferenced-value-may-be-null]
171+
return;
172+
}
173+
if (_stack == null) {
174+
_end = 10;
175+
_stack = new Iterator<?>[_end];
176+
} else {
177+
// grow by 50%, for most part
178+
_end += Math.min(4000, Math.max(20, _end>>1));
179+
_stack = Arrays.copyOf(_stack, _end);
180+
}
181+
_stack[_top++] = it;
182+
}
183+
184+
public Iterator<?> popOrNull() {
185+
if (_top == 0) {
186+
return null;
187+
}
188+
// note: could clean up stack but due to usage pattern, should not make
189+
// much difference since the whole stack is discarded after serialization done
190+
return _stack[--_top];
191+
}
121192
}
122193
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.fasterxml.jackson.databind.deser.dos;
2+
3+
import com.fasterxml.jackson.databind.BaseMapTest;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
7+
public class DeepJsonNodeSerTest extends BaseMapTest
8+
{
9+
private final ObjectMapper MAPPER = newJsonMapper();
10+
11+
public void testVeryDeepNodeSer() throws Exception
12+
{
13+
int depth = 9000;
14+
StringBuilder jsonString = new StringBuilder();
15+
jsonString.append("{");
16+
17+
for (int i=0; i < depth; i++) {
18+
jsonString.append(String.format("\"abc%s\": {", i));
19+
}
20+
21+
for (int i=0; i < depth; i++) {
22+
jsonString.append("}");
23+
}
24+
25+
jsonString.append("}");
26+
27+
JsonNode jsonNode = MAPPER.readTree(jsonString.toString());
28+
String json = jsonNode.toString();
29+
assertNotNull(json);
30+
}
31+
}

0 commit comments

Comments
 (0)