From ceb16e540530d78f3bb60e31577cb85c6bde21aa Mon Sep 17 00:00:00 2001 From: Weaselbot Date: Tue, 30 Jun 2026 12:09:26 -0400 Subject: [PATCH] schemagen: fix quadratic leading-zero strip in integer fallback Replace the O(k^2) loop that erased leading zeros one byte at a time from the front of a std::string with a single linear scan and one erase(0, n) call. Also adds regression tests for issue #34: - correctness cases for numbers with leading fractional zeros - a static check that the generated code no longer contains the quadratic pattern - a large-input case (100k leading zeros) that reproduces the vulnerable shape Closes #34 --- contrib/schemagen/test_schemagen.py | 49 +++++++++++++++++++++++ contrib/schemagen/weaseljson_schemagen.py | 6 ++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/contrib/schemagen/test_schemagen.py b/contrib/schemagen/test_schemagen.py index fdf195b..4fdd0ae 100644 --- a/contrib/schemagen/test_schemagen.py +++ b/contrib/schemagen/test_schemagen.py @@ -382,6 +382,8 @@ class SchemagenIntegerBoundaryTest(unittest.TestCase): ("1e-3", "WeaselJson_REJECT", 0), ("1000e-3", "WeaselJson_OK", 1), ("100.0e-2", "WeaselJson_OK", 1), + ("0.0001e4", "WeaselJson_OK", 1), + ("0.001e3", "WeaselJson_OK", 1), ("123.0", "WeaselJson_OK", 123), ("9e18", "WeaselJson_OK", 9000000000000000000), ("10e18", "WeaselJson_REJECT", 0), @@ -425,6 +427,7 @@ class SchemagenIntegerBoundaryTest(unittest.TestCase): ('{"age":-9223372036854775809}', "WeaselJson_REJECT", 0), ('{"age":1e3}', "WeaselJson_OK", 1000), ('{"age":2.0}', "WeaselJson_OK", 2), + ('{"age":0.0001e4}', "WeaselJson_OK", 1), ('{"age":0.001}', "WeaselJson_REJECT", 0), ( '{"age":-9223372036854775808.0}', @@ -468,6 +471,52 @@ class SchemagenIntegerBoundaryTest(unittest.TestCase): with tempfile.TemporaryDirectory() as tmpdir: self._compile_harness(tmpdir, schema, harness) + def test_integer_no_quadratic_leading_zero_loop(self): + """Regression test for issue #34: leading-zero stripping must not be quadratic.""" + with tempfile.TemporaryDirectory() as tmpdir: + schema_path = os.path.join(tmpdir, "schema.json") + with open(schema_path, "w") as fp: + json.dump({"type": "integer"}, fp) + result = subprocess.run( + [sys.executable, SCRIPT, schema_path], + capture_output=True, + text=True, + check=False, + ) + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertIn("parseJsonInt64", result.stdout) + self.assertNotIn("digits.erase(digits.begin())", result.stdout) + + def test_integer_large_fractional_leading_zeros(self): + """Numbers with many leading fractional zeros must parse correctly.""" + if not self.compiler: + self.skipTest("C++ compiler not available") + harness = textwrap.dedent( + """ + #include "gen.h" + #include + #include + int main() { + const int n = 100000; + std::string s = std::string("0.") + std::string(n - 1, '0') + "1e" + std::to_string(n); + test_schema::RootBuilder b; + WeaselJsonStatus st = b.feed(s.data(), static_cast(s.size())); + st = b.finish(); + if (st != WeaselJson_OK) { + std::printf("expected OK, got %d\\n", st); + return 1; + } + if (b.take() != 1) { + std::printf("expected value 1\\n"); + return 2; + } + return 0; + } + """ + ) + with tempfile.TemporaryDirectory() as tmpdir: + self._compile_harness(tmpdir, {"type": "integer"}, harness) + if __name__ == "__main__": unittest.main() diff --git a/contrib/schemagen/weaseljson_schemagen.py b/contrib/schemagen/weaseljson_schemagen.py index b70a51c..c64e514 100644 --- a/contrib/schemagen/weaseljson_schemagen.py +++ b/contrib/schemagen/weaseljson_schemagen.py @@ -1000,8 +1000,10 @@ private: }} int64_t finalExp = exp - fracDigits + trim; if (finalExp < 0) return false; - while (!digits.empty() && digits.front() == '0') digits.erase(digits.begin()); - if (digits.empty()) {{ out = 0; return true; }} + size_t leadingZeros = 0; + while (leadingZeros < digits.size() && digits[leadingZeros] == '0') ++leadingZeros; + if (leadingZeros == digits.size()) {{ out = 0; return true; }} + if (leadingZeros > 0) digits.erase(0, leadingZeros); constexpr uint64_t kMaxNeg = 9223372036854775808ULL; constexpr uint64_t kMaxPos = 9223372036854775807ULL;