Skip to content

Commit 20636b5

Browse files
authored
Implementation of loadAllElements with iterative fallback once max depth is reached (#103)
1 parent b635d5c commit 20636b5

File tree

4 files changed

+246
-17
lines changed

4 files changed

+246
-17
lines changed

src/main/kotlin/com/amazon/ionelement/api/IonUtils.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public fun IonElement.toIonValue(factory: ValueFactory): IonValue {
4242
* Bridge function that converts from the mutable [IonValue] to an [AnyElement].
4343
*
4444
* New code that does not need to integrate with uses of the mutable DOM should not use this.
45+
*
46+
* This will fail for IonDatagram if the IonDatagram does not contain exactly one user value.
4547
*/
4648
public fun IonValue.toIonElement(): AnyElement =
4749
this.system.newReader(this).use { reader ->

src/main/kotlin/com/amazon/ionelement/impl/IonElementLoaderImpl.kt

Lines changed: 146 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,19 @@ import com.amazon.ion.TextSpan
2525
import com.amazon.ion.system.IonReaderBuilder
2626
import com.amazon.ionelement.api.*
2727
import com.amazon.ionelement.impl.collections.*
28+
import java.util.ArrayDeque
29+
import java.util.ArrayList
30+
import kotlinx.collections.immutable.adapters.ImmutableListAdapter
2831

2932
internal class IonElementLoaderImpl(private val options: IonElementLoaderOptions) : IonElementLoader {
3033

34+
// TODO: It seems like some data can be read faster with a recursive approach, but other data is
35+
// faster with an iterative approach. Consider making this configurable. It probably doesn't
36+
// need to be finely-tune-able—just 0 or 100 (i.e. on/off) is probably sufficient.
37+
companion object {
38+
private const val MAX_RECURSION_DEPTH: Int = 100
39+
}
40+
3141
/**
3242
* Catches an [IonException] occurring in [block] and throws an [IonElementLoaderException] with
3343
* the current [IonLocation] of the fault, if one is available. Note that depending on the state of the
@@ -86,6 +96,10 @@ internal class IonElementLoaderImpl(private val options: IonElementLoaderOptions
8696
IonReaderBuilder.standard().build(ionText).use(::loadAllElements)
8797

8898
override fun loadCurrentElement(ionReader: IonReader): AnyElement {
99+
return loadCurrentElementRecursively(ionReader)
100+
}
101+
102+
private fun loadCurrentElementRecursively(ionReader: IonReader): AnyElement {
89103
return handleReaderException(ionReader) {
90104
val valueType = requireNotNull(ionReader.type) { "The IonReader was not positioned at an element." }
91105

@@ -128,26 +142,41 @@ internal class IonElementLoaderImpl(private val options: IonElementLoaderOptions
128142
IonType.BLOB -> BlobElementImpl(ionReader.newBytes(), annotations, metas)
129143
IonType.LIST -> {
130144
ionReader.stepIn()
131-
val list = ListElementImpl(loadAllElements(ionReader).toImmutableListUnsafe(), annotations, metas)
145+
val listContent = ArrayList<AnyElement>()
146+
if (ionReader.depth < MAX_RECURSION_DEPTH) {
147+
while (ionReader.next() != null) {
148+
listContent.add(loadCurrentElementRecursively(ionReader))
149+
}
150+
} else {
151+
loadAllElementsIteratively(ionReader, listContent as MutableList<Any>)
152+
}
132153
ionReader.stepOut()
133-
list
154+
ListElementImpl(listContent.toImmutableListUnsafe(), annotations, metas)
134155
}
135156
IonType.SEXP -> {
136157
ionReader.stepIn()
137-
val sexp = SexpElementImpl(loadAllElements(ionReader).toImmutableListUnsafe(), annotations, metas)
158+
val sexpContent = ArrayList<AnyElement>()
159+
if (ionReader.depth < MAX_RECURSION_DEPTH) {
160+
while (ionReader.next() != null) {
161+
sexpContent.add(loadCurrentElementRecursively(ionReader))
162+
}
163+
} else {
164+
loadAllElementsIteratively(ionReader, sexpContent as MutableList<Any>)
165+
}
138166
ionReader.stepOut()
139-
sexp
167+
SexpElementImpl(sexpContent.toImmutableListUnsafe(), annotations, metas)
140168
}
141169
IonType.STRUCT -> {
142-
val fields = mutableListOf<StructField>()
170+
val fields = ArrayList<StructField>()
143171
ionReader.stepIn()
144-
while (ionReader.next() != null) {
145-
fields.add(
146-
StructFieldImpl(
147-
ionReader.fieldName,
148-
loadCurrentElement(ionReader)
149-
)
150-
)
172+
if (ionReader.depth < MAX_RECURSION_DEPTH) {
173+
while (ionReader.next() != null) {
174+
val fieldName = ionReader.fieldName
175+
val element = loadCurrentElementRecursively(ionReader)
176+
fields.add(StructFieldImpl(fieldName, element))
177+
}
178+
} else {
179+
loadAllElementsIteratively(ionReader, fields as MutableList<Any>)
151180
}
152181
ionReader.stepOut()
153182
StructElementImpl(fields.toImmutableListUnsafe(), annotations, metas)
@@ -158,4 +187,109 @@ internal class IonElementLoaderImpl(private val options: IonElementLoaderOptions
158187
}.asAnyElement()
159188
}
160189
}
190+
191+
private fun loadAllElementsIteratively(ionReader: IonReader, into: MutableList<Any>) {
192+
// Intentionally not using a "recycling" stack because we have mutable lists that we are going to wrap as
193+
// ImmutableLists and then forget about the reference to the mutable list.
194+
val openContainerStack = ArrayDeque<MutableList<Any>>()
195+
var elements: MutableList<Any> = into
196+
197+
while (true) {
198+
val valueType = ionReader.next()
199+
200+
// End of container or input
201+
if (valueType == null) {
202+
// We're at the top (relative to where we started)
203+
if (elements === into) {
204+
return
205+
} else {
206+
ionReader.stepOut()
207+
elements = openContainerStack.pop()
208+
continue
209+
}
210+
}
211+
212+
// Read a value
213+
val annotations = ionReader.typeAnnotations!!.toImmutableListUnsafe()
214+
215+
var metas = EMPTY_METAS
216+
if (options.includeLocationMeta) {
217+
val location = ionReader.currentLocation()
218+
if (location != null) metas = location.toMetaContainer()
219+
}
220+
221+
if (ionReader.isNullValue) {
222+
elements.addContainerElement(ionReader, ionNull(valueType.toElementType(), annotations, metas).asAnyElement())
223+
continue
224+
} else when (valueType) {
225+
IonType.BOOL -> elements.addContainerElement(ionReader, BoolElementImpl(ionReader.booleanValue(), annotations, metas))
226+
IonType.INT -> {
227+
val intValue = when (ionReader.integerSize!!) {
228+
IntegerSize.BIG_INTEGER -> {
229+
val bigIntValue = ionReader.bigIntegerValue()
230+
if (bigIntValue !in RANGE_OF_LONG)
231+
BigIntIntElementImpl(bigIntValue, annotations, metas)
232+
else {
233+
LongIntElementImpl(ionReader.longValue(), annotations, metas)
234+
}
235+
}
236+
IntegerSize.LONG,
237+
IntegerSize.INT -> LongIntElementImpl(ionReader.longValue(), annotations, metas)
238+
}
239+
elements.addContainerElement(ionReader, intValue)
240+
}
241+
IonType.FLOAT -> elements.addContainerElement(ionReader, FloatElementImpl(ionReader.doubleValue(), annotations, metas))
242+
IonType.DECIMAL -> elements.addContainerElement(ionReader, DecimalElementImpl(ionReader.decimalValue(), annotations, metas))
243+
IonType.TIMESTAMP -> elements.addContainerElement(ionReader, TimestampElementImpl(ionReader.timestampValue(), annotations, metas))
244+
IonType.STRING -> elements.addContainerElement(ionReader, StringElementImpl(ionReader.stringValue(), annotations, metas))
245+
IonType.SYMBOL -> elements.addContainerElement(ionReader, SymbolElementImpl(ionReader.stringValue(), annotations, metas))
246+
IonType.CLOB -> elements.addContainerElement(ionReader, ClobElementImpl(ionReader.newBytes(), annotations, metas))
247+
IonType.BLOB -> elements.addContainerElement(ionReader, BlobElementImpl(ionReader.newBytes(), annotations, metas))
248+
IonType.LIST -> {
249+
val listContent = ArrayList<AnyElement>()
250+
// `listContent` gets wrapped in an `ImmutableListWrapper` so that we can create a ListElementImpl
251+
// right away. Then, we add child elements to `ListContent`. Technically, this is a violation of the
252+
// contract for `ImmutableListAdapter`, but it is safe to do so here because no reads will occur
253+
// after we are done modifying the backing list.
254+
// Same thing applies for `sexpContent` and `structContent` in their respective branches.
255+
elements.addContainerElement(ionReader, ListElementImpl(ImmutableListAdapter(listContent), annotations, metas))
256+
ionReader.stepIn()
257+
openContainerStack.push(elements)
258+
elements = listContent as MutableList<Any>
259+
}
260+
IonType.SEXP -> {
261+
val sexpContent = ArrayList<AnyElement>()
262+
elements.addContainerElement(ionReader, SexpElementImpl(ImmutableListAdapter(sexpContent), annotations, metas))
263+
ionReader.stepIn()
264+
openContainerStack.push(elements)
265+
elements = sexpContent as MutableList<Any>
266+
}
267+
IonType.STRUCT -> {
268+
val structContent = ArrayList<StructField>()
269+
elements.addContainerElement(
270+
ionReader,
271+
StructElementImpl(
272+
ImmutableListAdapter(structContent),
273+
annotations,
274+
metas
275+
)
276+
)
277+
ionReader.stepIn()
278+
openContainerStack.push(elements)
279+
elements = structContent as MutableList<Any>
280+
}
281+
IonType.DATAGRAM -> error("IonElementLoaderImpl does not know what to do with IonType.DATAGRAM")
282+
IonType.NULL -> error("IonType.NULL branch should be unreachable")
283+
}
284+
}
285+
}
286+
287+
private fun MutableList<Any>.addContainerElement(ionReader: IonReader, value: AnyElement) {
288+
val fieldName = ionReader.fieldName
289+
if (fieldName != null) {
290+
add(StructFieldImpl(fieldName, value))
291+
} else {
292+
add(value)
293+
}
294+
}
161295
}

src/main/kotlin/com/amazon/ionelement/impl/StructElementImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ internal class StructElementImpl(
3131
) : AnyElementBase(), StructElement {
3232

3333
override val type: ElementType get() = ElementType.STRUCT
34-
override val size = allFields.size
34+
override val size: Int get() = allFields.size
3535

3636
// Note that we are not using `by lazy` here because it requires 2 additional allocations and
3737
// has been demonstrated to significantly increase memory consumption!

src/test/kotlin/com/amazon/ionelement/IonElementLoaderTests.kt

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@
1515

1616
package com.amazon.ionelement
1717

18+
import com.amazon.ion.Decimal
19+
import com.amazon.ion.system.IonReaderBuilder
1820
import com.amazon.ionelement.api.*
1921
import com.amazon.ionelement.util.INCLUDE_LOCATION_META
2022
import com.amazon.ionelement.util.ION
2123
import com.amazon.ionelement.util.IonElementLoaderTestCase
2224
import com.amazon.ionelement.util.convertToString
25+
import java.math.BigInteger
2326
import org.junit.jupiter.api.Assertions.assertEquals
27+
import org.junit.jupiter.api.Test
28+
import org.junit.jupiter.api.assertThrows
2429
import org.junit.jupiter.params.ParameterizedTest
2530
import org.junit.jupiter.params.provider.MethodSource
2631

@@ -45,6 +50,8 @@ class IonElementLoaderTests {
4550
// Converting from IonValue to IonElement should result in an IonElement that is equivalent to the
4651
// parsed IonElement
4752
assertEquals(parsedIonValue.toIonElement(), parsedIonElement)
53+
54+
assertEquals(tc.expectedElement, parsedIonElement)
4855
}
4956

5057
companion object {
@@ -57,15 +64,29 @@ class IonElementLoaderTests {
5764

5865
IonElementLoaderTestCase("1", ionInt(1)),
5966

60-
IonElementLoaderTestCase("existence::42", ionInt(1).withAnnotations("existence")),
67+
IonElementLoaderTestCase("9223372036854775807", ionInt(Long.MAX_VALUE)),
6168

62-
IonElementLoaderTestCase("\"some string\"", ionString("some string")),
69+
IonElementLoaderTestCase("9223372036854775808", ionInt(BigInteger.valueOf(Long.MAX_VALUE) + BigInteger.ONE)),
70+
71+
IonElementLoaderTestCase("existence::42", ionInt(42).withAnnotations("existence")),
72+
73+
IonElementLoaderTestCase("0.", ionDecimal(Decimal.ZERO)),
74+
75+
IonElementLoaderTestCase("1e0", ionFloat(1.0)),
6376

6477
IonElementLoaderTestCase("2019-10-30T04:23:59Z", ionTimestamp("2019-10-30T04:23:59Z")),
6578

79+
IonElementLoaderTestCase("\"some string\"", ionString("some string")),
80+
81+
IonElementLoaderTestCase("'some symbol'", ionSymbol("some symbol")),
82+
83+
IonElementLoaderTestCase("{{\"some clob\"}}", ionClob("some clob".encodeToByteArray())),
84+
85+
IonElementLoaderTestCase("{{ }}", ionBlob(byteArrayOf())),
86+
6687
IonElementLoaderTestCase("[1, 2, 3]", ionListOf(ionInt(1), ionInt(2), ionInt(3))),
6788

68-
IonElementLoaderTestCase("(1 2 3)", ionListOf(ionInt(1), ionInt(2), ionInt(3))),
89+
IonElementLoaderTestCase("(1 2 3)", ionSexpOf(ionInt(1), ionInt(2), ionInt(3))),
6990

7091
IonElementLoaderTestCase(
7192
"{ foo: 1, bar: 2, bat: 3 }",
@@ -74,7 +95,79 @@ class IonElementLoaderTests {
7495
"bar" to ionInt(2),
7596
"bat" to ionInt(3)
7697
)
77-
)
98+
),
99+
100+
// Nested container cases
101+
IonElementLoaderTestCase("((null.list))", ionSexpOf(ionSexpOf(ionNull(ElementType.LIST)))),
102+
IonElementLoaderTestCase("(1 (2 3))", ionSexpOf(ionInt(1), ionSexpOf(ionInt(2), ionInt(3)))),
103+
IonElementLoaderTestCase("{foo:[1]}", ionStructOf("foo" to ionListOf(ionInt(1)))),
104+
IonElementLoaderTestCase("[{foo:1}]", ionListOf(ionStructOf("foo" to ionInt(1)))),
105+
IonElementLoaderTestCase("{foo:{bar:1}}", ionStructOf("foo" to ionStructOf("bar" to ionInt(1)))),
106+
IonElementLoaderTestCase("{foo:[{}]}", ionStructOf("foo" to ionListOf(ionStructOf(emptyList())))),
107+
IonElementLoaderTestCase("[{}]", ionListOf(ionStructOf(emptyList()))),
108+
IonElementLoaderTestCase("[{}, {}]", ionListOf(ionStructOf(emptyList()), ionStructOf(emptyList()))),
109+
IonElementLoaderTestCase("[{foo:1, bar: 2}]", ionListOf(ionStructOf("foo" to ionInt(1), "bar" to ionInt(2)))),
110+
IonElementLoaderTestCase(
111+
"{foo:[{bar:({})}]}",
112+
ionStructOf("foo" to ionListOf(ionStructOf("bar" to ionSexpOf(ionStructOf(emptyList())))))
113+
),
78114
)
79115
}
116+
117+
@Test
118+
fun `regardless of depth, no StackOverflowError is thrown`() {
119+
// Throws StackOverflowError in [email protected] and prior versions when there's ~4k nested containers
120+
// Run for every container type to ensure that they all correctly fall back to the iterative impl.
121+
122+
val listData = "[".repeat(999999) + "]".repeat(999999)
123+
loadAllElements(listData)
124+
125+
val sexpData = "(".repeat(999999) + ")".repeat(999999)
126+
loadAllElements(sexpData)
127+
128+
val structData = "{a:".repeat(999999) + "b" + "}".repeat(999999)
129+
loadAllElements(structData)
130+
}
131+
132+
@ParameterizedTest
133+
@MethodSource("parametersForDemoTest")
134+
fun `deeply nested values should be loaded correctly`(tc: IonElementLoaderTestCase) {
135+
// Wrap everything in many layers of Ion lists so that we can be sure to trigger the iterative fallback.
136+
val nestingLevels = 500
137+
val textIon = "[".repeat(nestingLevels) + tc.textIon + "]".repeat(nestingLevels)
138+
var expectedElement = tc.expectedElement
139+
repeat(nestingLevels) { expectedElement = ionListOf(expectedElement) }
140+
141+
val parsedIonValue = ION.singleValue(textIon)
142+
val parsedIonElement = loadSingleElement(textIon, INCLUDE_LOCATION_META)
143+
144+
// Text generated from both should match
145+
assertEquals(convertToString(parsedIonValue), parsedIonElement.toString())
146+
147+
// Converting from IonElement to IonValue results in an IonValue that is equivalent to the parsed IonValue
148+
assertEquals(parsedIonElement.toIonValue(ION), parsedIonValue)
149+
150+
// Converting from IonValue to IonElement should result in an IonElement that is equivalent to the
151+
// parsed IonElement
152+
assertEquals(parsedIonValue.toIonElement(), parsedIonElement)
153+
154+
assertEquals(expectedElement, parsedIonElement)
155+
}
156+
157+
@Test
158+
fun `loadCurrentElement throws exception when not positioned on a value`() {
159+
val reader = IonReaderBuilder.standard().build("foo")
160+
// We do not advance to the first value in the reader.
161+
assertThrows<IllegalArgumentException> { loadCurrentElement(reader) }
162+
}
163+
164+
@Test
165+
fun `loadSingleElement throws exception when no values in reader`() {
166+
assertThrows<IllegalArgumentException> { loadSingleElement("") }
167+
}
168+
169+
@Test
170+
fun `loadSingleElement throws exception when more than one values in reader`() {
171+
assertThrows<IllegalArgumentException> { loadSingleElement("a b") }
172+
}
80173
}

0 commit comments

Comments
 (0)