From fb740774613a2e3d5ff9534c2c8df9d3ed7417b9 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Sat, 4 Jul 2026 17:18:13 +0800 Subject: [PATCH] fix: validate CLI numeric limits Motivation: sjsonnet accepted invalid numeric CLI limits for stack frames, trace cropping, and parser recursion depth. Non-positive stack limits created a broken runtime setting, negative trace limits behaved like an undocumented full-trace setting, and negative parser recursion depth reached the parser as Parsing exceeded maximum recursion depth of -1. Modification: Validate --max-stack >= 1, --max-trace >= 0, and --max-parser-recursion-depth >= 0 after CLI parsing and before constructing Settings. Add JVM CLI regression tests for invalid stack, trace, and parser-depth values, and preserve the boundary behavior that --max-trace 0 keeps a full trace and --max-parser-recursion-depth 0 still allows non-recursive expressions. Result: Invalid numeric CLI limits now fail before evaluation or parsing with user-facing CLI errors. Stack and trace behavior matches C++ jsonnet and go-jsonnet for shared flags, while the sjsonnet-specific parser-depth flag now rejects only the invalid negative range. References: C++ jsonnet cmd/jsonnet.cpp rejects --max-stack < 1 and --max-trace < 0. go-jsonnet cmd/jsonnet/cmd.go rejects --max-stack < 1 and --max-trace < 0. sjsonnet readme.md documents --max-parser-recursion-depth without a negative-value meaning. --- .../sjsonnet/SjsonnetMainBase.scala | 15 +++++++ .../test/src-jvm/sjsonnet/MainTests.scala | 41 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala b/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala index 4b97d65e..d6444943 100644 --- a/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala +++ b/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala @@ -195,6 +195,21 @@ object SjsonnetMainBase { Left("ERROR: cannot use --no-trailing-newline with --yaml-stream") else Right(()) } + _ <- { + if (config.maxStack < 1) Left(s"ERROR: invalid --max-stack value: ${config.maxStack}") + else Right(()) + } + _ <- { + if (config.maxTrace < 0) Left(s"ERROR: invalid --max-trace value: ${config.maxTrace}") + else Right(()) + } + _ <- { + if (config.maxParserRecursionDepth < 0) + Left( + s"ERROR: invalid --max-parser-recursion-depth value: ${config.maxParserRecursionDepth}" + ) + else Right(()) + } file <- Right(config.file) debugStats = if (config.debugStats.value) { val s = new DebugStats; statsToReport = s; s } diff --git a/sjsonnet/test/src-jvm/sjsonnet/MainTests.scala b/sjsonnet/test/src-jvm/sjsonnet/MainTests.scala index 1c01cfb1..2c348bd7 100644 --- a/sjsonnet/test/src-jvm/sjsonnet/MainTests.scala +++ b/sjsonnet/test/src-jvm/sjsonnet/MainTests.scala @@ -147,6 +147,47 @@ object MainTests extends TestSuite { assert(err.contains("Max stack frames exceeded.")) } + test("maxStackRejectsNonPositiveValues") { + val (zeroRes, zeroOut, zeroErr) = runMain("--exec", "1", "--max-stack", "0") + assert(zeroRes == 1) + assert(zeroOut.isEmpty) + assert(zeroErr.contains("ERROR: invalid --max-stack value: 0")) + assert(!zeroErr.contains("Max stack frames exceeded.")) + + val (negativeRes, negativeOut, negativeErr) = runMain("--exec", "1", "--max-stack", "-1") + assert(negativeRes == 1) + assert(negativeOut.isEmpty) + assert(negativeErr.contains("ERROR: invalid --max-stack value: -1")) + assert(!negativeErr.contains("Max stack frames exceeded.")) + } + + test("maxTraceRejectsNegativeValues") { + val (res, out, err) = runMain("--exec", "error 'x'", "--max-trace", "-1") + assert(res == 1) + assert(out.isEmpty) + assert(err.contains("ERROR: invalid --max-trace value: -1")) + assert(!err.contains("sjsonnet.Error")) + } + + test("maxParserRecursionDepthRejectsNegativeValues") { + val (res, out, err) = runMain("--exec", "1", "--max-parser-recursion-depth", "-1") + assert(res == 1) + assert(out.isEmpty) + assert(err.contains("ERROR: invalid --max-parser-recursion-depth value: -1")) + assert(!err.contains("Parsing exceeded maximum recursion depth of -1")) + } + + test("maxParserRecursionDepthZeroAllowsNonRecursiveExpressions") { + val (res, out, err) = runMain("--exec", "1", "--max-parser-recursion-depth", "0") + assert((res, out, err) == ((0, "1\n", ""))) + + val (nestedRes, nestedOut, nestedErr) = + runMain("--exec", "[1]", "--max-parser-recursion-depth", "0") + assert(nestedRes == 1) + assert(nestedOut.isEmpty) + assert(nestedErr.contains("Parsing exceeded maximum recursion depth of 0")) + } + test("maxStackDoesNotCountTailRecursiveCalls") { val (res, out, err) = runMain( "--exec",