diff --git a/sjsonnet/src/sjsonnet/ByteRenderer.scala b/sjsonnet/src/sjsonnet/ByteRenderer.scala index f10b180c..ad65bdbd 100644 --- a/sjsonnet/src/sjsonnet/ByteRenderer.scala +++ b/sjsonnet/src/sjsonnet/ByteRenderer.scala @@ -222,13 +222,13 @@ class ByteRenderer(out: OutputStream = new java.io.ByteArrayOutputStream(), inde materializeDirectArr(xs, matDepth + 1, ctx) else // Fall back to generic visitor path for extremely deep nesting - Materializer.apply0(v, this)(evaluator) + Materializer.materializeStackless(v, this, ctx)(evaluator) case 6 => // TAG_OBJ val obj = v.asInstanceOf[Val.Obj] if (matDepth < ctx.recursiveDepthLimit) materializeDirectObj(obj, matDepth + 1, ctx) else - Materializer.apply0(v, this)(evaluator) + Materializer.materializeStackless(v, this, ctx)(evaluator) case 7 => // TAG_FUNC val s = v.asInstanceOf[Val.Func] Error.fail( diff --git a/sjsonnet/src/sjsonnet/Materializer.scala b/sjsonnet/src/sjsonnet/Materializer.scala index f168b3d0..9892e3da 100644 --- a/sjsonnet/src/sjsonnet/Materializer.scala +++ b/sjsonnet/src/sjsonnet/Materializer.scala @@ -333,7 +333,7 @@ abstract class Materializer { // Iterative materialization for deep nesting. Used as a fallback when recursive depth exceeds // the recursive depth limit. Uses an explicit ArrayDeque stack to avoid StackOverflowError. - private def materializeStackless[T]( + private[sjsonnet] def materializeStackless[T]( v: Val, visitor: Visitor[T, T], ctx: Materializer.MaterializeContext)(implicit evaluator: EvalScope): T = { diff --git a/sjsonnet/test/src/sjsonnet/RendererTests.scala b/sjsonnet/test/src/sjsonnet/RendererTests.scala index 65577f25..a06c58af 100644 --- a/sjsonnet/test/src/sjsonnet/RendererTests.scala +++ b/sjsonnet/test/src/sjsonnet/RendererTests.scala @@ -1,5 +1,6 @@ package sjsonnet +import java.io.ByteArrayOutputStream import utest._ object RendererTests extends TestSuite { @@ -65,6 +66,36 @@ object RendererTests extends TestSuite { ujson.transform(ujson.Num(1e15), new Renderer()).toString ==> "1000000000000000" } + test("byteRendererFallbackPreservesCycleContext") { + val interpreter = new Interpreter( + Map(), + Map(), + DummyPath(), + Importer.empty, + parseCache = new DefaultParseCache, + settings = Settings.default.copy(materializeRecursiveDepthLimit = 2, maxMaterializeDepth = 2) + ) + val value = interpreter.evaluate( + """local o = { + | a: std.repeat("x", 10000), + | z: { b: o }, + |}; + |{ root: o }""".stripMargin, + DummyPath("(memory)") + ) match { + case Right(v) => v + case Left(err) => throw new Exception(Error.formatError(err)) + } + val out = new ByteArrayOutputStream + val e = interpreter.materialize(value, new ByteRenderer(out)) match { + case Left(err) => Error.formatError(err) + case Right(_) => throw new Exception("Expected recursive value materialization error") + } + assert(e.contains("Stackoverflow while materializing, possibly due to recursive value")) + // Losing the outer materialization context re-renders `o.a` before detecting the cycle. + assert(out.size() < 15000) + } + test("indentZero") { // indent=0 should produce newlines but no spaces ujson.transform(ujson.Arr(1, 2), new Renderer(indent = 0)).toString ==>