12 Commits
Author SHA1 Message Date
weaselbot 43e3c9904f Reject negative lengths in WeaselJsonParser_parse
`Parser3::parse` previously formed `buf + len` immediately, so passing a
negative `len` from the C API caused undefined pointer arithmetic. Add an
explicit `len < 0` check that returns `WeaselJson_REJECT` (and makes the
rejected state sticky) before any `buf + len` computation.

Also document the non-negative length precondition in the public header
and add a regression test.

Closes #24
2026-06-22 02:26:30 -04:00
andrew 5e18347e35 Merge pull request 'schemagen: add C++20 keywords to the C++ keyword allow-list' (#22) from weaselbot/weaseljson:weaselbot/issue-18 into main
Reviewed-on: weaselab/weaseljson#22
2026-06-22 00:54:04 +00:00
weaselbot 9cd74631b6 schemagen: add C++20 keywords to the C++ keyword allow-list
Add concept, consteval, constinit, co_await, co_return, co_yield,
requires, module, and import to _CPP_KEYWORDS so property names that
happen to be C++20 keywords get sanitized with a trailing underscore.

Closes #18
2026-06-21 12:52:07 -04:00
andrew 05185eb3ee Merge pull request 'Make parser reject state sticky after WeaselJson_REJECT' (#15) from weaselbot/weaseljson:weaselbot/issue-12 into main
Reviewed-on: weaselab/weaseljson#15
2026-06-19 17:17:10 +00:00
weaselbot 9839104635 Make parser reject state sticky after WeaselJson_REJECT
After the parser returns WeaselJson_REJECT once, subsequent calls to
WeaselJsonParser_parse (including the final len==0 EOF call) must keep
returning WeaselJson_REJECT instead of potentially reporting OK.

- Add a `rejected` flag to Parser3.
- Clear the flag in reset().
- Check the flag at the start of parse() and immediately return REJECT.
- Set the flag whenever a continuation returns REJECT.

Add a test covering the exact reproduction from issue #12.
2026-06-19 13:08:28 -04:00
andrew b641489a59 Merge pull request 'schemagen: support objects with >32 properties' (#9) from weaselbot/weaseljson:weaselbot/issue-3 into main
Reviewed-on: weaselab/weaseljson#9
2026-06-18 22:23:52 +00:00
weaselbot 2ad15708eb ci: register schemagen regression tests with ctest
Remove the standalone "Run schemagen tests" workflow step and instead
add the big-schema regression test to contrib/schemagen/CMakeLists.txt
so ctest picks it up alongside schemagen_example. Update README to
document both `ctest` and the convenience `./run_tests.sh`.

Closes #3
2026-06-18 16:32:50 -04:00
weaselbot ca474e3f99 schemagen: support objects with more than 32 properties
Replace the 32-bit `uint32_t seen` bitmask with a `std::vector<uint64_t>`
bitset sized to the actual field count.  `requiredMask` is now a vector of
the same word count, and required-field checks use per-word masking so that
optional fields do not cause false rejections.

Adds big.schema.json + test_big.cpp regression tests covering the issue
reproducer (40 required properties, missing/duplicate at index 32), plus
run_tests.sh to exercise both test_gen.cpp and test_big.cpp.

Fixes #3
2026-06-18 16:31:26 -04:00
andrew 7c1c18fe6f Merge pull request 'Reset transient parser state in Parser3::reset' (#10) from weaselbot/weaseljson:weaselbot/issue-2 into main
Reviewed-on: weaselab/weaseljson#10
2026-06-18 20:09:32 +00:00
andrew 3d9772357d Merge pull request 'schemagen: keep null elements in arrays with nullable item types' (#8) from weaselbot/weaseljson:weaselbot/issue-5 into main
Reviewed-on: weaselab/weaseljson#8
2026-06-18 20:07:48 +00:00
weaselbot 919b89c842 Parser3::reset: clear inKey and other per-parse transient state
WeaselJsonParser_reset is documented to restore the parser to its
newly-created state, but reset() only rewound the symbol stack.  The
inKey flag and transient DFA/codepoint state from the previous parse
leaked into the next parse, so a top-level string after a mid-key
reset was delivered via on_key_data instead of on_string_data.

Reset inKey to false and clear utf8Codepoint, utf16Surrogate,
minCodepoint, numDfa, and strDfa so the next document starts fresh.

Add a test that reproduces the reported misrouting.
2026-06-18 10:34:43 -04:00
weaselbot 47f1077100 schemagen: keep null elements in arrays with nullable item types
For array types whose items are nullable ({"type": ["T", "null"]}),
the generated builder previously called valueComplete() on cbNull() without
appending anything to the owning vector. Null entries were silently dropped,
so vector indices no longer matched JSON array indices.

Generate isArrayKind() / appendNull() helpers and have cbNull() append a
default-constructed element when the current frame is an array. For
std::optional<T> items this appends an empty optional; for std::unique_ptr<T>
items it appends a null pointer. Add nullable string/integer array fields to
the example schema and test coverage to verify indices are preserved.

Fixes #5
2026-06-18 10:18:11 -04:00
13 changed files with 651 additions and 23 deletions
+4
View File
@@ -1,2 +1,6 @@
build build
.cache .cache
contrib/schemagen/gen.h
contrib/schemagen/big.h
contrib/schemagen/test_gen
contrib/schemagen/test_big
+22
View File
@@ -12,6 +12,8 @@ add_test(
set(SCHEMAGEN_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/weaseljson_schemagen.py) set(SCHEMAGEN_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/weaseljson_schemagen.py)
set(EXAMPLE_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/example.schema.json) set(EXAMPLE_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/example.schema.json)
set(GEN_H ${CMAKE_CURRENT_BINARY_DIR}/gen.h) set(GEN_H ${CMAKE_CURRENT_BINARY_DIR}/gen.h)
set(BIG_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/big.schema.json)
set(BIG_H ${CMAKE_CURRENT_BINARY_DIR}/big.h)
add_custom_command( add_custom_command(
OUTPUT ${GEN_H} OUTPUT ${GEN_H}
@@ -20,7 +22,15 @@ add_custom_command(
DEPENDS ${SCHEMAGEN_SCRIPT} ${EXAMPLE_SCHEMA} DEPENDS ${SCHEMAGEN_SCRIPT} ${EXAMPLE_SCHEMA}
COMMENT "Generating gen.h from example.schema.json") COMMENT "Generating gen.h from example.schema.json")
add_custom_command(
OUTPUT ${BIG_H}
COMMAND ${Python3_EXECUTABLE} ${SCHEMAGEN_SCRIPT} ${BIG_SCHEMA} -o ${BIG_H}
--namespace big_schema
DEPENDS ${SCHEMAGEN_SCRIPT} ${BIG_SCHEMA}
COMMENT "Generating big.h from big.schema.json")
add_custom_target(schemagen_gen_h DEPENDS ${GEN_H}) add_custom_target(schemagen_gen_h DEPENDS ${GEN_H})
add_custom_target(schemagen_big_h DEPENDS ${BIG_H})
add_executable(schemagen_example ${CMAKE_CURRENT_SOURCE_DIR}/test_gen.cpp) add_executable(schemagen_example ${CMAKE_CURRENT_SOURCE_DIR}/test_gen.cpp)
target_include_directories(schemagen_example target_include_directories(schemagen_example
@@ -29,7 +39,19 @@ target_link_libraries(schemagen_example PRIVATE ${PROJECT_NAME})
target_compile_options(schemagen_example PRIVATE -Wno-switch-enum) target_compile_options(schemagen_example PRIVATE -Wno-switch-enum)
add_dependencies(schemagen_example schemagen_gen_h) add_dependencies(schemagen_example schemagen_gen_h)
add_executable(schemagen_big ${CMAKE_CURRENT_SOURCE_DIR}/test_big.cpp)
target_include_directories(schemagen_big PRIVATE include
${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(schemagen_big PRIVATE ${PROJECT_NAME})
target_compile_options(schemagen_big PRIVATE -Wno-switch-enum)
add_dependencies(schemagen_big schemagen_big_h)
add_test( add_test(
NAME schemagen_example NAME schemagen_example
COMMAND schemagen_example COMMAND schemagen_example
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
add_test(
NAME schemagen_big
COMMAND schemagen_big
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
+25
View File
@@ -73,3 +73,28 @@ union `type` lists other than `["T", "null"]`, non-string enums, and remote
cannot hold partial digits. cannot hold partial digits.
- `test_gen.cpp` generates from `example.schema.json` and exercises the parser - `test_gen.cpp` generates from `example.schema.json` and exercises the parser
byte-by-byte (covering chunked strings/numbers), plus the rejection cases. byte-by-byte (covering chunked strings/numbers), plus the rejection cases.
## Testing
The schemagen tests are registered with CTest and run as part of the default
`ctest` invocation from the build directory:
```sh
cmake -S . -B build
make -C build -j "$(nproc)"
cd build
ctest --output-on-failure
```
For local development you can also use the convenience script:
```sh
cd contrib/schemagen
./run_tests.sh
```
Both regenerate the example parser (`gen.h`) and a regression parser with 40
required properties (`big.h`), compile `test_gen.cpp` and `test_big.cpp`, and run
them. `test_big.cpp` specifically covers issue #3: it checks that a 40-property
object accepts all fields, rejects a missing field at index 32, and rejects
duplicate keys around the 32-bit boundary.
+168
View File
@@ -0,0 +1,168 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"p0": {
"type": "string"
},
"p1": {
"type": "string"
},
"p2": {
"type": "string"
},
"p3": {
"type": "string"
},
"p4": {
"type": "string"
},
"p5": {
"type": "string"
},
"p6": {
"type": "string"
},
"p7": {
"type": "string"
},
"p8": {
"type": "string"
},
"p9": {
"type": "string"
},
"p10": {
"type": "string"
},
"p11": {
"type": "string"
},
"p12": {
"type": "string"
},
"p13": {
"type": "string"
},
"p14": {
"type": "string"
},
"p15": {
"type": "string"
},
"p16": {
"type": "string"
},
"p17": {
"type": "string"
},
"p18": {
"type": "string"
},
"p19": {
"type": "string"
},
"p20": {
"type": "string"
},
"p21": {
"type": "string"
},
"p22": {
"type": "string"
},
"p23": {
"type": "string"
},
"p24": {
"type": "string"
},
"p25": {
"type": "string"
},
"p26": {
"type": "string"
},
"p27": {
"type": "string"
},
"p28": {
"type": "string"
},
"p29": {
"type": "string"
},
"p30": {
"type": "string"
},
"p31": {
"type": "string"
},
"p32": {
"type": "string"
},
"p33": {
"type": "string"
},
"p34": {
"type": "string"
},
"p35": {
"type": "string"
},
"p36": {
"type": "string"
},
"p37": {
"type": "string"
},
"p38": {
"type": "string"
},
"p39": {
"type": "string"
}
},
"required": [
"p0",
"p1",
"p2",
"p3",
"p4",
"p5",
"p6",
"p7",
"p8",
"p9",
"p10",
"p11",
"p12",
"p13",
"p14",
"p15",
"p16",
"p17",
"p18",
"p19",
"p20",
"p21",
"p22",
"p23",
"p24",
"p25",
"p26",
"p27",
"p28",
"p29",
"p30",
"p31",
"p32",
"p33",
"p34",
"p35",
"p36",
"p37",
"p38",
"p39"
]
}
+18
View File
@@ -71,6 +71,24 @@
}, },
"tree": { "tree": {
"$ref": "#/$defs/Node" "$ref": "#/$defs/Node"
},
"nullable_hobbies": {
"type": "array",
"items": {
"type": [
"string",
"null"
]
}
},
"nullable_scores": {
"type": "array",
"items": {
"type": [
"integer",
"null"
]
}
} }
}, },
"$defs": { "$defs": {
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# Build and run the schemagen regression tests.
# Expects weaseljson to be built at ../../build (the default CMake build dir).
set -euo pipefail
cd "$(dirname "$0")"
python3 weaseljson_schemagen.py example.schema.json -o gen.h --namespace weasel_schema
python3 weaseljson_schemagen.py big.schema.json -o big.h --namespace big_schema
g++ -std=c++20 -I../../include -I. test_gen.cpp \
-L../../build -lweaseljson \
-Wl,-rpath,'$ORIGIN'/../../build \
-o test_gen
g++ -std=c++20 -I../../include -I. test_big.cpp \
-L../../build -lweaseljson \
-Wl,-rpath,'$ORIGIN'/../../build \
-o test_big
./test_gen
./test_big
+140
View File
@@ -0,0 +1,140 @@
// Regression test for issue #3: objects with more than 32 properties.
#include <cassert>
#include <cstdio>
#include <string>
#include "big.h"
using namespace big_schema;
static WeaselJsonStatus parseStrided(RootBuilder &b, std::string in) {
for (size_t i = 0; i < in.size(); ++i) {
char c = in[i];
WeaselJsonStatus s = b.feed(&c, 1);
if (s != WeaselJson_AGAIN)
return s;
}
return b.finish();
}
static int failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
++failures; \
} \
} while (0)
static void expectReject(const std::string &json, const char *what) {
RootBuilder b;
WeaselJsonStatus s = parseStrided(b, json);
if (s == WeaselJson_REJECT) {
printf("ok reject: %s\n", what);
} else {
printf("FAIL expected reject (%s) got status %d for: %s\n", what, s,
json.c_str());
++failures;
}
}
static std::string all40() {
std::string json = "{";
for (int i = 0; i < 40; ++i) {
if (i)
json += ",";
json += "\"p" + std::to_string(i) + "\":\"v" + std::to_string(i) + "\"";
}
json += "}";
return json;
}
int main() {
// All 40 required fields present -> accepted, values land in right slots.
{
RootBuilder b;
std::string json = all40();
WeaselJsonStatus s = parseStrided(b, json);
CHECK(s == WeaselJson_OK);
if (s == WeaselJson_OK) {
Root r = b.take();
CHECK(r.p0 == "v0");
CHECK(r.p31 == "v31");
CHECK(r.p32 == "v32");
CHECK(r.p39 == "v39");
printf("ok all 40 required fields accepted\n");
}
}
// Missing field p32 (index 32) must be detected, not aliased to bit 0.
{
std::string json = all40();
// Remove the p32 entry: find its substring and erase it.
std::string entry = "\"p32\":\"v32\"";
size_t pos = json.find(entry);
assert(pos != std::string::npos);
// Remove the trailing comma before it if present, or the leading comma
// after it.
if (pos > 0 && json[pos - 1] == ',') {
json.erase(pos - 1, entry.size() + 1);
} else if (pos + entry.size() < json.size() &&
json[pos + entry.size()] == ',') {
json.erase(pos, entry.size() + 1);
} else {
json.erase(pos, entry.size());
}
expectReject(json, "missing required field p32 (index 32)");
}
// Missing p0 still rejected.
{
std::string json = all40();
std::string entry = "\"p0\":\"v0\"";
size_t pos = json.find(entry);
assert(pos != std::string::npos);
if (json[pos + entry.size()] == ',') {
json.erase(pos, entry.size() + 1);
} else {
json.erase(pos, entry.size());
}
expectReject(json, "missing required field p0");
}
// Duplicate keys p0 and p32 must both be detected.
{
std::string json = all40();
// Insert p32 again right after the existing p32 entry.
std::string entry = "\"p32\":\"v32\"";
size_t pos = json.find(entry);
assert(pos != std::string::npos);
json.insert(pos + entry.size(), ",\"p32\":\"dup\"");
expectReject(json, "duplicate key p32 (index 32)");
}
// Duplicate keys p0 and p31 cover boundary bits.
{
std::string json = all40();
std::string entry = "\"p0\":\"v0\"";
size_t pos = json.find(entry);
assert(pos != std::string::npos);
json.insert(pos + entry.size(), ",\"p0\":\"dup\"");
expectReject(json, "duplicate key p0");
}
// Duplicate p31/p32 around the 32-bit boundary.
{
std::string json = all40();
std::string entry = "\"p31\":\"v31\"";
size_t pos = json.find(entry);
assert(pos != std::string::npos);
json.insert(pos + entry.size(), ",\"p31\":\"dup\"");
expectReject(json, "duplicate key p31");
}
if (failures == 0) {
printf("\nALL TESTS PASSED\n");
return 0;
}
printf("\n%d FAILURE(S)\n", failures);
return 1;
}
+13 -1
View File
@@ -86,7 +86,9 @@ int main() {
"value": 1, "value": 1,
"children": [ { "value": 2 }, { "value": 3 } ], "children": [ { "value": 2 }, { "value": 3 } ],
"next": { "value": 99 } "next": { "value": 99 }
} },
"nullable_hobbies": [null, "math", null, "lace", null],
"nullable_scores": [null, 10, null, 20, null]
})"; })";
RootBuilder b; RootBuilder b;
WeaselJsonStatus s = parseStrided(b, json); WeaselJsonStatus s = parseStrided(b, json);
@@ -116,6 +118,16 @@ int main() {
CHECK(r.tree && r.tree->children && (*r.tree->children)[0].value == 2); CHECK(r.tree && r.tree->children && (*r.tree->children)[0].value == 2);
CHECK(r.tree && r.tree->children && (*r.tree->children)[1].value == 3); CHECK(r.tree && r.tree->children && (*r.tree->children)[1].value == 3);
CHECK(r.tree && r.tree->next && r.tree->next->value == 99); CHECK(r.tree && r.tree->next && r.tree->next->value == 99);
CHECK(r.nullable_hobbies.has_value() && r.nullable_hobbies->size() == 5);
CHECK(r.nullable_hobbies && !(*r.nullable_hobbies)[0] &&
(*r.nullable_hobbies)[1] && *(*r.nullable_hobbies)[1] == "math" &&
!(*r.nullable_hobbies)[2] && (*r.nullable_hobbies)[3] &&
*(*r.nullable_hobbies)[3] == "lace" && !(*r.nullable_hobbies)[4]);
CHECK(r.nullable_scores.has_value() && r.nullable_scores->size() == 5);
CHECK(r.nullable_scores && !(*r.nullable_scores)[0] &&
(*r.nullable_scores)[1] && *(*r.nullable_scores)[1] == 10 &&
!(*r.nullable_scores)[2] && (*r.nullable_scores)[3] &&
*(*r.nullable_scores)[3] == 20 && !(*r.nullable_scores)[4]);
printf("ok happy path (byte-strided)\n"); printf("ok happy path (byte-strided)\n");
} }
} }
+38
View File
@@ -49,5 +49,43 @@ class SchemagenEnumTest(unittest.TestCase):
self.assertIn("enum class Role : int { admin, user, guest };", stdout) self.assertIn("enum class Role : int { admin, user, guest };", stdout)
class SchemagenKeywordTest(unittest.TestCase):
def run_schemagen(self, schema, args=None):
"""Run schemagen on a schema dict. Returns (returncode, stdout, stderr)."""
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fp:
json.dump(schema, fp)
schema_path = fp.name
try:
cmd = [sys.executable, SCRIPT, schema_path]
if args:
cmd.extend(args)
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
return result.returncode, result.stdout, result.stderr
finally:
os.unlink(schema_path)
def test_cpp20_keywords_are_sanitized(self):
"""C++20 keywords used as JSON property names must be suffixed."""
keywords = [
"concept",
"consteval",
"constinit",
"co_await",
"co_return",
"co_yield",
"requires",
"module",
"import",
]
schema = {"type": "object", "properties": {}}
for kw in keywords:
schema["properties"][kw] = {"type": "string"}
rc, stdout, stderr = self.run_schemagen(schema)
self.assertEqual(rc, 0, msg=stderr)
for kw in keywords:
self.assertIn(f"std::optional<std::string> {kw}_;", stdout)
self.assertNotIn(f"std::optional<std::string> {kw};", stdout)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+92 -18
View File
@@ -133,9 +133,15 @@ _CPP_KEYWORDS = {
"catch", "catch",
"char", "char",
"class", "class",
"concept",
"const", "const",
"consteval",
"constinit",
"constexpr", "constexpr",
"continue", "continue",
"co_await",
"co_return",
"co_yield",
"decltype", "decltype",
"default", "default",
"delete", "delete",
@@ -152,9 +158,11 @@ _CPP_KEYWORDS = {
"friend", "friend",
"goto", "goto",
"if", "if",
"import",
"inline", "inline",
"int", "int",
"long", "long",
"module",
"namespace", "namespace",
"new", "new",
"not", "not",
@@ -166,6 +174,7 @@ _CPP_KEYWORDS = {
"public", "public",
"register", "register",
"return", "return",
"requires",
"short", "short",
"signed", "signed",
"sizeof", "sizeof",
@@ -690,19 +699,35 @@ namespace {ns} {{"""
lines.append(" }") lines.append(" }")
return "\n".join(lines) return "\n".join(lines)
def _bitmask(self, obj, pred): def _fieldcount(self):
mask = 0 lines = [" int fieldCount(Kind k) const {", " switch (k) {"]
for i, fld in enumerate(obj.fields): for name, obj in self.b.objects.items():
if pred(fld): lines.append(f" case Kind::{name}: return {len(obj.fields)};")
mask |= 1 << i lines.append(" default: return 0;")
return mask lines.append(" }")
lines.append(" }")
return "\n".join(lines)
def _reqmask(self): def _reqmask(self):
lines = [" uint32_t requiredMask(Kind k) const {", " switch (k) {"] lines = [
" const std::vector<uint64_t> &requiredMask(Kind k) const {",
" switch (k) {",
]
for name, obj in self.b.objects.items(): for name, obj in self.b.objects.items():
m = self._bitmask(obj, lambda f: f.required) words = (len(obj.fields) + 63) // 64
lines.append(f" case Kind::{name}: return {hex(m)}u;") req = [0] * words
lines.append(" default: return 0u;") for i, fld in enumerate(obj.fields):
if fld.required:
req[i >> 6] |= 1 << (i & 63)
init = ", ".join(f"{hex(w)}u" for w in req) if words else ""
lines.append(f" case Kind::{name}: {{")
lines.append(f" static const std::vector<uint64_t> m = {{ {init} }};")
lines.append(" return m;")
lines.append(" }")
lines.append(" default: {")
lines.append(" static const std::vector<uint64_t> empty;")
lines.append(" return empty;")
lines.append(" }")
lines.append(" }") lines.append(" }")
lines.append(" }") lines.append(" }")
return "\n".join(lines) return "\n".join(lines)
@@ -718,6 +743,34 @@ namespace {ns} {{"""
" }" " }"
) )
def _is_array_kind(self):
arrs = [n for n, _ in self.arr_types]
if not arrs:
return " bool isArrayKind(Kind) const { return false; }"
cases = " ".join(f"case Kind::{n}:" for n in arrs)
return (
" bool isArrayKind(Kind k) const {\n"
f" switch (k) {{ {cases} return true; default: return false; }}\n"
" }"
)
def _append_null(self):
lines = [
" void appendNull(Frame &f) {",
" switch (f.kind) {",
]
for name, tarr in self.arr_types:
vectype = self.base_cpp(tarr)
lines.append(f" case Kind::{name}: {{")
lines.append(f" auto *v = ({vectype} *)f.dest;")
lines.append(" v->emplace_back();")
lines.append(" return;")
lines.append(" }")
lines.append(" default: return;")
lines.append(" }")
lines.append(" }")
return "\n".join(lines)
def _is_strict(self): def _is_strict(self):
strict = [n for n, o in self.b.objects.items() if o.strict] strict = [n for n, o in self.b.objects.items() if o.strict]
if not strict: if not strict:
@@ -755,6 +808,11 @@ namespace {ns} {{"""
if root_kind is not None and root_cat == event_cat: if root_kind is not None and root_cat == event_cat:
lines.append(" if (stack_.empty()) {") lines.append(" if (stack_.empty()) {")
lines.append(f" stack_.push_back(Frame{{{root_kind}, &result_}});") lines.append(f" stack_.push_back(Frame{{{root_kind}, &result_}});")
if event_cat == "Obj":
lines.append(" {")
lines.append(f" int n = fieldCount({root_kind});")
lines.append(" stack_.back().seen.assign((n + 63) / 64, 0);")
lines.append(" }")
lines.append(" return;") lines.append(" return;")
lines.append(" }") lines.append(" }")
else: else:
@@ -770,6 +828,10 @@ namespace {ns} {{"""
lines.append(f" if (si.cat != Cat::{event_cat}) {{ reject(); return; }}") lines.append(f" if (si.cat != Cat::{event_cat}) {{ reject(); return; }}")
lines.append(" void *p = engage(f, true);") lines.append(" void *p = engage(f, true);")
lines.append(" stack_.push_back(Frame{si.child, p});") lines.append(" stack_.push_back(Frame{si.child, p});")
lines.append(" if (isObjectKind(si.child)) {")
lines.append(" int n = fieldCount(si.child);")
lines.append(" stack_.back().seen.assign((n + 63) / 64, 0);")
lines.append(" }")
return "\n".join(lines) return "\n".join(lines)
def _builder(self, order): def _builder(self, order):
@@ -822,7 +884,7 @@ private:
Kind kind; Kind kind;
void *dest; void *dest;
int field = -1; // object: selected field (-1 want key, -2 skip) int field = -1; // object: selected field (-1 want key, -2 skip)
uint32_t seen = 0; // bitmask of populated fields std::vector<uint64_t> seen; // populated field bitset (object frames)
}}; }};
static constexpr int kWantKey = -1; static constexpr int kWantKey = -1;
static constexpr int kSkip = -2; static constexpr int kSkip = -2;
@@ -857,7 +919,7 @@ private:
scratch_.clear(); scratch_.clear();
{(" if (p.kind == Kind::RootScalar) { stack_.pop_back(); done_ = true; return; }" if root_kind is None else "")} {(" if (p.kind == Kind::RootScalar) { stack_.pop_back(); done_ = true; return; }" if root_kind is None else "")}
if (isObjectKind(p.kind)) {{ if (isObjectKind(p.kind)) {{
if (p.field >= 0) p.seen |= (1u << p.field); if (p.field >= 0) p.seen[p.field >> 6] |= (1ull << (p.field & 63));
p.field = kWantKey; p.field = kWantKey;
}} }}
}} }}
@@ -867,16 +929,19 @@ private:
}} }}
void cbEndObject() {{ void cbEndObject() {{
if (error_) return; if (error_) return;
Frame f = stack_.back(); Frame &f = stack_.back();
if (f.kind == Kind::Skip) {{ if (f.kind == Kind::Skip) {{
stack_.pop_back(); stack_.pop_back();
if (stack_.empty() || stack_.back().kind != Kind::Skip) valueComplete(); if (stack_.empty() || stack_.back().kind != Kind::Skip) valueComplete();
return; return;
}} }}
if (isObjectKind(f.kind) && if (isObjectKind(f.kind)) {{
(f.seen & requiredMask(f.kind)) != requiredMask(f.kind)) {{ const auto &req = requiredMask(f.kind);
reject(); bool missing = false;
return; for (size_t i = 0; i < req.size(); ++i) {{
if ((f.seen[i] & req[i]) != req[i]) {{ missing = true; break; }}
}}
if (missing) {{ reject(); return; }}
}} }}
stack_.pop_back(); stack_.pop_back();
valueComplete(); valueComplete();
@@ -909,7 +974,9 @@ private:
f.field = kSkip; f.field = kSkip;
return; return;
}} }}
if (f.seen & (1u << idx)) {{ reject(); return; }} // duplicate key if (f.seen[idx >> 6] & (1ull << (idx & 63))) {{
reject(); return; // duplicate key
}}
f.field = idx; f.field = idx;
}} }}
@@ -992,6 +1059,10 @@ private:
valueComplete(); valueComplete();
}} }}
{self._is_array_kind()}
{self._append_null()}
void cbNull() {{ void cbNull() {{
if (error_) return; if (error_) return;
Frame &f = stack_.back(); Frame &f = stack_.back();
@@ -999,6 +1070,7 @@ private:
SlotInfo si = slotInfoG(f); SlotInfo si = slotInfoG(f);
if (si.cat == Cat::Skip) {{ valueComplete(); return; }} if (si.cat == Cat::Skip) {{ valueComplete(); return; }}
if (!si.nullable) {{ reject(); return; }} if (!si.nullable) {{ reject(); return; }}
if (isArrayKind(f.kind)) appendNull(f); // keep null array elements
valueComplete(); // leave optional empty / pointer null valueComplete(); // leave optional empty / pointer null
}} }}
@@ -1029,6 +1101,8 @@ private:
{self._matchkey()} {self._matchkey()}
{self._fieldcount()}
{self._reqmask()} {self._reqmask()}
{self._is_object_kind()} {self._is_object_kind()}
+2 -1
View File
@@ -64,7 +64,8 @@ void WeaselJsonParser_destroy(WeaselJsonParser *parser);
/** Incrementally parse `len` more bytes starting at `buf`. `buf` may be /** Incrementally parse `len` more bytes starting at `buf`. `buf` may be
* modified. Call with `len` 0 to indicate end of data. `buf` may be null if * modified. Call with `len` 0 to indicate end of data. `buf` may be null if
* `len` is 0 */ * `len` is 0. `len` must not be negative; a negative length is treated as a
* rejected input. */
WeaselJsonStatus WeaselJsonParser_parse(WeaselJsonParser *parser, char *buf, WeaselJsonStatus WeaselJsonParser_parse(WeaselJsonParser *parser, char *buf,
int len); int len);
+26 -2
View File
@@ -138,6 +138,13 @@ struct Parser3 {
void reset() { void reset() {
stackPtr = stack(); stackPtr = stack();
std::ignore = push({N_VALUE, N_WHITESPACE, T_EOF}); std::ignore = push({N_VALUE, N_WHITESPACE, T_EOF});
inKey = false;
rejected = false;
utf8Codepoint = 0;
utf16Surrogate = 0;
minCodepoint = 0;
numDfa.reset();
strDfa.reset();
} }
// Used for flushing pending data with on_*_data callbacks // Used for flushing pending data with on_*_data callbacks
@@ -155,6 +162,7 @@ struct Parser3 {
NumDfa numDfa; NumDfa numDfa;
Utf8Dfa strDfa; Utf8Dfa strDfa;
bool inKey = false; bool inKey = false;
bool rejected = false;
#ifndef HAS_MUSTTAIL #ifndef HAS_MUSTTAIL
char *stashBufForTrampoline; char *stashBufForTrampoline;
@@ -1056,18 +1064,34 @@ constexpr inline struct ContinuationTable {
inline WeaselJsonStatus Parser3::parse(char *buf, int len) { inline WeaselJsonStatus Parser3::parse(char *buf, int len) {
this->dataBegin = this->writeBuf = buf; this->dataBegin = this->writeBuf = buf;
if (this->rejected) [[unlikely]] {
return WeaselJson_REJECT;
}
if (len < 0) [[unlikely]] {
this->rejected = true;
return WeaselJson_REJECT;
}
#ifdef HAS_MUSTTAIL #ifdef HAS_MUSTTAIL
// The continuation returns a value in 0..3 here (kBounce is only used by the // The continuation returns a value in 0..3 here (kBounce is only used by the
// no-musttail trampoline below), so the conversion back to the enum is in // no-musttail trampoline below), so the conversion back to the enum is in
// range. // range.
return WeaselJsonStatus( ContinuationStatus status =
symbolTables.continuations[top()](this, buf, buf + len)); symbolTables.continuations[top()](this, buf, buf + len);
if (status == WeaselJson_REJECT) {
this->rejected = true;
}
return WeaselJsonStatus(status);
#else #else
this->stashBufForTrampoline = buf; this->stashBufForTrampoline = buf;
ContinuationStatus result; ContinuationStatus result;
while ((result = symbolTables.continuations[top()]( while ((result = symbolTables.continuations[top()](
this, stashBufForTrampoline, buf + len)) == kBounce) this, stashBufForTrampoline, buf + len)) == kBounce)
; ;
if (result == WeaselJson_REJECT) {
this->rejected = true;
}
return WeaselJsonStatus(result); return WeaselJsonStatus(result);
#endif #endif
} }
+80
View File
@@ -202,6 +202,31 @@ TEST_CASE("parser3") {
} }
} }
TEST_CASE("rejected state is sticky") {
auto c = noopCallbacks();
auto *parser = WeaselJsonParser_create(1024, &c, nullptr, 0);
REQUIRE(parser != nullptr);
// Feed a valid prefix, then an invalid byte. The parser must reject.
std::string chunk1 = "1";
REQUIRE(WeaselJsonParser_parse(parser, chunk1.data(), chunk1.size()) ==
WeaselJson_AGAIN);
std::string chunk2 = "x";
REQUIRE(WeaselJsonParser_parse(parser, chunk2.data(), chunk2.size()) ==
WeaselJson_REJECT);
// After a reject, every later call must also reject, including the EOF
// finish call that would otherwise return OK.
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) == WeaselJson_REJECT);
// Further data chunks must also stay rejected.
std::string chunk3 = " ";
REQUIRE(WeaselJsonParser_parse(parser, chunk3.data(), chunk3.size()) ==
WeaselJson_REJECT);
WeaselJsonParser_destroy(parser);
}
TEST_CASE("create rejects too-small stack") { TEST_CASE("create rejects too-small stack") {
auto c = noopCallbacks(); auto c = noopCallbacks();
// The parser needs room for the bootstrap symbols pushed by reset(); a stack // The parser needs room for the bootstrap symbols pushed by reset(); a stack
@@ -221,8 +246,63 @@ TEST_CASE("create rejects too-small stack") {
WeaselJsonParser_destroy(parser); WeaselJsonParser_destroy(parser);
} }
TEST_CASE("parse rejects negative length") {
auto c = noopCallbacks();
auto *parser = WeaselJsonParser_create(1024, &c, nullptr, 0);
REQUIRE(parser != nullptr);
// A negative length must not cause pointer arithmetic UB. It should be
// rejected, and the rejected state should remain sticky.
char buf[10] = "hello";
REQUIRE(WeaselJsonParser_parse(parser, buf, -1) == WeaselJson_REJECT);
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) == WeaselJson_REJECT);
WeaselJsonParser_destroy(parser);
}
TEST_CASE("streaming") { testStreaming(json); } TEST_CASE("streaming") { testStreaming(json); }
TEST_CASE("reset clears inKey and transient state") {
struct State {
std::string stringData;
std::string keyData;
} state;
auto c = noopCallbacks();
c.on_string_data = +[](void *p, const char *buf, int len, int /*done*/) {
((State *)p)->stringData.append(buf, len);
};
c.on_key_data = +[](void *p, const char *buf, int len, int /*done*/) {
((State *)p)->keyData.append(buf, len);
};
auto *parser = WeaselJsonParser_create(1024, &c, &state, 0);
REQUIRE(parser != nullptr);
{
std::string chunk = "{\"ab";
REQUIRE(WeaselJsonParser_parse(parser, chunk.data(), chunk.size()) ==
WeaselJson_AGAIN);
}
// Reset mid-key: the next top-level string must be delivered as a string,
// not appended to the aborted key.
WeaselJsonParser_reset(parser);
state.stringData.clear();
state.keyData.clear();
{
std::string chunk = "\"hello\"";
REQUIRE(WeaselJsonParser_parse(parser, chunk.data(), chunk.size()) ==
WeaselJson_AGAIN);
}
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) == WeaselJson_OK);
CHECK(state.stringData == "hello");
CHECK(state.keyData.empty());
WeaselJsonParser_destroy(parser);
}
void doTestUnescapingUtf8(std::string const &escaped, void doTestUnescapingUtf8(std::string const &escaped,
std::string const &expected, int stride, int flags) { std::string const &expected, int stride, int flags) {
CAPTURE(escaped); CAPTURE(escaped);