From df693ef4c97866310cd3b87e26a7d4e538708036 Mon Sep 17 00:00:00 2001 From: Weaselbot Date: Wed, 24 Jun 2026 14:30:36 -0400 Subject: [PATCH] schemagen: reject scalar root values when root schema is object/array The generated RootBuilder crashed (undefined behavior on std::vector::back()) when a JSON document's root value was a scalar or null while the schema declared a non-nullable object or array root. The stack starts empty for object/array roots, but cbStringData, cbNumberData, and cbBool called stack_.back() without checking for an empty stack. Add an empty-stack guard to the three scalar callbacks so they reject instead of crashing. cbNull already handles the empty-stack case. Regression tests added for: - non-nullable object root rejecting null, boolean, number, and string roots - nullable object root rejecting scalar roots - nullable array root rejecting scalar roots Closes #16 --- contrib/schemagen/test_gen.cpp | 4 +++ contrib/schemagen/test_nullable_root.cpp | 40 +++++++++++++++++++++++ contrib/schemagen/weaseljson_schemagen.py | 3 ++ 3 files changed, 47 insertions(+) diff --git a/contrib/schemagen/test_gen.cpp b/contrib/schemagen/test_gen.cpp index 577e983..8dd23a3 100644 --- a/contrib/schemagen/test_gen.cpp +++ b/contrib/schemagen/test_gen.cpp @@ -184,6 +184,10 @@ int main() { expectReject(R"({"name":"x","age":1,"address":{"zip":5}})", "missing required nested 'city'"); expectReject(R"([1,2,3])", "array where object expected (root)"); + expectReject("null", "null where object expected (root)"); + expectReject("true", "boolean where object expected (root)"); + expectReject("123", "number where object expected (root)"); + expectReject(R"("hi")", "string where object expected (root)"); expectReject(R"({"name":"x","age":1,)", "truncated / invalid json"); if (failures == 0) { diff --git a/contrib/schemagen/test_nullable_root.cpp b/contrib/schemagen/test_nullable_root.cpp index 11a9a88..f3b39a1 100644 --- a/contrib/schemagen/test_nullable_root.cpp +++ b/contrib/schemagen/test_nullable_root.cpp @@ -61,6 +61,18 @@ static void expectReject(nullable_object::RootBuilder &b, std::string in, } } +static void expectReject(nullable_array::RootBuilder &b, std::string in, + const char *what) { + WeaselJsonStatus s = parseStrided(b, in); + if (s == WeaselJson_REJECT) { + printf("ok reject: %s\n", what); + } else { + printf("FAIL expected reject (%s) got status %d for: %s\n", what, s, + in.c_str()); + ++failures; + } +} + int main() { // ---- nullable root object: valid document ---- { @@ -149,6 +161,34 @@ int main() { } } + // ---- nullable root object: scalar values rejected ---- + { + nullable_object::RootBuilder b; + expectReject(b, "true", "boolean where nullable object expected (root)"); + } + { + nullable_object::RootBuilder b; + expectReject(b, "123", "number where nullable object expected (root)"); + } + { + nullable_object::RootBuilder b; + expectReject(b, R"("hi")", "string where nullable object expected (root)"); + } + + // ---- nullable root array: scalar values rejected ---- + { + nullable_array::RootBuilder b; + expectReject(b, "true", "boolean where nullable array expected (root)"); + } + { + nullable_array::RootBuilder b; + expectReject(b, "123", "number where nullable array expected (root)"); + } + { + nullable_array::RootBuilder b; + expectReject(b, R"("hi")", "string where nullable array expected (root)"); + } + if (failures == 0) { printf("\nALL TESTS PASSED\n"); return 0; diff --git a/contrib/schemagen/weaseljson_schemagen.py b/contrib/schemagen/weaseljson_schemagen.py index 8c8b72b..491d7e0 100644 --- a/contrib/schemagen/weaseljson_schemagen.py +++ b/contrib/schemagen/weaseljson_schemagen.py @@ -1098,6 +1098,7 @@ private: void cbStringData(const char *buf, int len, int done) {{ if (error_) return; + if (stack_.empty()) {{ reject(); return; }} Frame &f = stack_.back(); SlotInfo si = slotInfoG(f); if (si.cat == Cat::Str) {{ @@ -1124,6 +1125,7 @@ private: void cbNumberData(const char *buf, int len, int done) {{ if (error_) return; + if (stack_.empty()) {{ reject(); return; }} Frame &f = stack_.back(); SlotInfo si = slotInfoG(f); if (si.cat != Cat::Int && si.cat != Cat::Dbl) {{ reject(); return; }} @@ -1157,6 +1159,7 @@ private: void cbBool(bool value) {{ if (error_) return; + if (stack_.empty()) {{ reject(); return; }} Frame &f = stack_.back(); SlotInfo si = slotInfoG(f); if (si.cat != Cat::Bool) {{ reject(); return; }}