diff --git a/sjsonnet/src/sjsonnet/ByteRenderer.scala b/sjsonnet/src/sjsonnet/ByteRenderer.scala index 9fcb7d8a..7b1d9171 100644 --- a/sjsonnet/src/sjsonnet/ByteRenderer.scala +++ b/sjsonnet/src/sjsonnet/ByteRenderer.scala @@ -26,12 +26,34 @@ class ByteRenderer(out: OutputStream = new java.io.ByteArrayOutputStream(), inde var newlineBuffered = false // Track empty state per nesting level. Bit i = 1 means level i has seen a value. - // Supports up to 64 levels of nesting (realistic JSON rarely exceeds ~20). - private var emptyBits: Long = 0L + // Uses BitSet (auto-growing) instead of a Long bitmask, which only supported 64 levels + // and caused silent corruption at depth >= 64 due to Java's shift masking (1L << 64 == 1L << 0). + // Growable bitmask supporting arbitrary nesting depth (each Long holds 64 bits). + // Replaces the previous Long-only bitmask which overflowed at depth >= 64. + private var emptyBits: Array[Long] = new Array[Long](2) // supports up to 128 levels initially + + @inline private def ensureCapacity(d: Int): Unit = { + val idx = d >> 6 + if (idx >= emptyBits.length) { + val newArr = new Array[Long](math.max(idx + 1, emptyBits.length * 2)) + System.arraycopy(emptyBits, 0, newArr, 0, emptyBits.length) + emptyBits = newArr + } + } - @inline private def markNonEmpty(): Unit = emptyBits |= (1L << depth) - @inline private def isEmpty: Boolean = (emptyBits & (1L << depth)) == 0L - @inline private def resetEmpty(): Unit = emptyBits &= ~(1L << depth) + @inline private def markNonEmpty(): Unit = { + ensureCapacity(depth) + emptyBits(depth >> 6) |= (1L << (depth & 63)) + } + @inline private def isEmpty: Boolean = { + val idx = depth >> 6 + idx >= emptyBits.length || (emptyBits(idx) & (1L << (depth & 63))) == 0L + } + @inline private def resetEmpty(): Unit = { + val idx = depth >> 6 + if (idx < emptyBits.length) + emptyBits(idx) &= ~(1L << (depth & 63)) + } private var stringValueCount = 0 private var stringValueCacheKeys: Array[String] = null diff --git a/sjsonnet/test/resources/new_test_suite/byterenderer_deep_nesting.jsonnet b/sjsonnet/test/resources/new_test_suite/byterenderer_deep_nesting.jsonnet new file mode 100644 index 00000000..9a47231f --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/byterenderer_deep_nesting.jsonnet @@ -0,0 +1,10 @@ +// Regression test: ByteRenderer empty-tracking bitmask overflow at depth >= 64. +local nest(n, leaf) = + std.foldl(function(acc, _) { inner: acc }, std.range(1, n), leaf); +local deep100 = nest(100, "leaf"); +local drillDown(obj, n) = + std.foldl(function(acc, _) acc.inner, std.range(1, n), obj); +assert std.assertEqual(drillDown(deep100, 100), "leaf") : "depth-100 leaf reachable"; +local deep200 = nest(200, 42); +assert std.assertEqual(drillDown(deep200, 200), 42) : "depth-200 leaf reachable"; +true diff --git a/sjsonnet/test/resources/new_test_suite/byterenderer_deep_nesting.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/byterenderer_deep_nesting.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/byterenderer_deep_nesting.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/src/sjsonnet/RendererTests.scala b/sjsonnet/test/src/sjsonnet/RendererTests.scala index 508f5493..6c46820f 100644 --- a/sjsonnet/test/src/sjsonnet/RendererTests.scala +++ b/sjsonnet/test/src/sjsonnet/RendererTests.scala @@ -247,6 +247,36 @@ object RendererTests extends TestSuite { escape(sb, unicode = true) ==> "\"a\\nb\"" escape(new java.lang.StringBuilder("plain"), unicode = true) ==> "\"plain\"" } + test("byteRendererDeepNestingBeyond64") { + def buildNested(depth: Int): ujson.Value = { + var v: ujson.Value = ujson.Str("leaf") + for (_ <- 0 until depth) { v = ujson.Obj("k" -> v) } + v + } + val nested100 = buildNested(100) + val out = new ByteArrayOutputStream() + ujson.transform(nested100, new ByteRenderer(out, indent = 3)) + val rendered = new String(out.toByteArray, java.nio.charset.StandardCharsets.UTF_8) + val lines = rendered.split('\n') + val closingBraceLines = lines.filter(_.trim == "}") + assert(closingBraceLines.length >= 99) + assert(rendered.contains("\"leaf\"")) + assert(!lines.exists(_.trim.contains("} }"))) + } + + test("byteRendererDeepNesting200") { + def buildNestedArr(depth: Int): ujson.Value = { + var v: ujson.Value = ujson.Num(42) + for (_ <- 0 until depth) { v = ujson.Arr(v) } + v + } + val nested200 = buildNestedArr(200) + val out = new ByteArrayOutputStream() + ujson.transform(nested200, new ByteRenderer(out, indent = 2)) + val rendered = new String(out.toByteArray, java.nio.charset.StandardCharsets.UTF_8) + val closingBracketLines = rendered.split('\n').filter(_.trim == "]") + assert(closingBracketLines.length == 200) + } }