Author SHA1 Message Date
weaselbot bf3f2fe810 Avoid nullptr subtraction using intptr_t casts
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (pull_request) Successful in 51s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (pull_request) Successful in 53s
CI / pre-commit (pull_request) Successful in 52s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (pull_request) Successful in 1m30s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (pull_request) Successful in 1m27s
Andrew's review on the previous fix noted that the nullptr checks produced slightly worse codegen. Replace the pointer subtraction with intptr_t subtraction, which avoids the undefined behaviour of subtracting two null pointers without introducing extra branches.
2026-06-29 15:14:36 -04:00
weaselbot bd53e57b8e Avoid nullptr subtraction when flushing scalars at EOF
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (pull_request) Successful in 52s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (pull_request) Successful in 50s
CI / pre-commit (pull_request) Successful in 51s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (pull_request) Successful in 1m30s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (pull_request) Successful in 1m24s
When a scalar ends exactly at a chunk boundary, Parser3::parse resets
dataBegin and writeBuf to the new buf at the start of every call. On the
EOF call buf is null, so both pointers become null. The final
flushNumber/flushString then computed len as buf - dataBegin, i.e.
nullptr - nullptr, which is undefined behaviour in C++.

Compute the flush length safely: if dataBegin is null (or, for raw mode,
buf is null), treat the length as zero. Use an empty string literal as a
non-null data pointer for the zero-length, done=true callback so callers
still receive the completion signal.

Add a regression test covering a number that fills its chunk exactly and
is finalized by an EOF call.

Fixes #40
2026-06-29 13:57:58 -04:00
13 changed files with 64 additions and 613 deletions
+2 -3
View File
@@ -60,9 +60,8 @@ into the result, so it is non-movable.
## Not supported (rejected at generation time, no fallback) ## Not supported (rejected at generation time, no fallback)
`oneOf` / `anyOf` / `allOf` / `not` / `if`-`then`-`else`, `oneOf` / `anyOf` / `allOf` / `not` / `if`-`then`-`else`,
`patternProperties`, `additionalProperties` absent or set to `true`, `patternProperties`, `additionalProperties` with a schema (typed map),
`additionalProperties` with a schema (typed map), `prefixItems` (tuples), `additionalProperties: true`, `prefixItems` (tuples), `const`,
`const`,
`dependentSchemas`/`dependentRequired`, union `type` lists other than `dependentSchemas`/`dependentRequired`, union `type` lists other than
`["T", "null"]`, non-string enums, and remote (`$ref` to other documents). `["T", "null"]`, non-string enums, and remote (`$ref` to other documents).
-1
View File
@@ -56,7 +56,6 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"additionalProperties": false,
"required": [ "required": [
"name" "name"
], ],
-9
View File
@@ -68,15 +68,6 @@ int main() {
expectReject(json, "unknown key in strict root"); expectReject(json, "unknown key in strict root");
} }
// ---- invalid stack size is rejected without crashing ----
{
RootBuilder b(-1);
char buf[] = "null";
WeaselJsonStatus s = b.feed(buf, sizeof(buf) - 1);
CHECK(s == WeaselJson_REJECT);
printf("ok invalid stack size rejected, not crashed\n");
}
{ {
std::string json = R"({ std::string json = R"({
"name": "Ada É", "name": "Ada É",
+9 -400
View File
@@ -32,7 +32,6 @@ class SchemagenEnumTest(unittest.TestCase):
def test_colliding_enum_values_deduplicate(self): def test_colliding_enum_values_deduplicate(self):
schema = { schema = {
"type": "object", "type": "object",
"additionalProperties": False,
"properties": {"role": {"enum": ["foo-bar", "foo_bar"]}}, "properties": {"role": {"enum": ["foo-bar", "foo_bar"]}},
} }
rc, stdout, stderr = self.run_schemagen(schema) rc, stdout, stderr = self.run_schemagen(schema)
@@ -46,7 +45,6 @@ class SchemagenEnumTest(unittest.TestCase):
def test_distinct_enum_values_generate(self): def test_distinct_enum_values_generate(self):
schema = { schema = {
"type": "object", "type": "object",
"additionalProperties": False,
"properties": {"role": {"enum": ["admin", "user", "guest"]}}, "properties": {"role": {"enum": ["admin", "user", "guest"]}},
} }
rc, stdout, stderr = self.run_schemagen(schema) rc, stdout, stderr = self.run_schemagen(schema)
@@ -82,7 +80,7 @@ class SchemagenKeywordTest(unittest.TestCase):
"module", "module",
"import", "import",
] ]
schema = {"type": "object", "additionalProperties": False, "properties": {}} schema = {"type": "object", "properties": {}}
for kw in keywords: for kw in keywords:
schema["properties"][kw] = {"type": "string"} schema["properties"][kw] = {"type": "string"}
rc, stdout, stderr = self.run_schemagen(schema) rc, stdout, stderr = self.run_schemagen(schema)
@@ -166,18 +164,11 @@ class SchemagenCollisionTest(unittest.TestCase):
def test_kind_enum_does_not_duplicate_arr0(self): def test_kind_enum_does_not_duplicate_arr0(self):
schema = { schema = {
"type": "object", "type": "object",
"additionalProperties": False,
"properties": { "properties": {
"arr": {"type": "array", "items": {"type": "string"}}, "arr": {"type": "array", "items": {"type": "string"}},
"obj": {"$ref": "#/$defs/Arr0"}, "obj": {"$ref": "#/$defs/Arr0"},
}, },
"$defs": { "$defs": {"Arr0": {"type": "object", "properties": {}}},
"Arr0": {
"type": "object",
"additionalProperties": False,
"properties": {},
}
},
} }
out = self.generate_and_compile(schema) out = self.generate_and_compile(schema)
self.assertIn("struct Arr0", out) self.assertIn("struct Arr0", out)
@@ -192,13 +183,7 @@ class SchemagenCollisionTest(unittest.TestCase):
schema = { schema = {
"type": "array", "type": "array",
"items": {"$ref": "#/$defs/Skip"}, "items": {"$ref": "#/$defs/Skip"},
"$defs": { "$defs": {"Skip": {"type": "object", "properties": {}}},
"Skip": {
"type": "object",
"additionalProperties": False,
"properties": {},
}
},
} }
out = self.generate_and_compile(schema) out = self.generate_and_compile(schema)
self.assertIn("struct Skip", out) self.assertIn("struct Skip", out)
@@ -214,7 +199,6 @@ class SchemagenCollisionTest(unittest.TestCase):
"""Regression test for issue #17: enum and array $defs must be reused.""" """Regression test for issue #17: enum and array $defs must be reused."""
schema = { schema = {
"type": "object", "type": "object",
"additionalProperties": False,
"properties": { "properties": {
"role1": {"$ref": "#/$defs/Role"}, "role1": {"$ref": "#/$defs/Role"},
"role2": {"$ref": "#/$defs/Role"}, "role2": {"$ref": "#/$defs/Role"},
@@ -268,14 +252,17 @@ class SchemagenAdditionalPropertiesTest(unittest.TestCase):
self.assertNotEqual(rc, 0) self.assertNotEqual(rc, 0)
self.assertIn("additionalProperties: true is not supported", stderr) self.assertIn("additionalProperties: true is not supported", stderr)
def test_additional_properties_absent_rejected(self): def test_additional_properties_absent_defaults_to_strict(self):
schema = { schema = {
"type": "object", "type": "object",
"properties": {"name": {"type": "string"}}, "properties": {"name": {"type": "string"}},
} }
rc, stdout, stderr = self.run_schemagen(schema) rc, stdout, stderr = self.run_schemagen(schema)
self.assertNotEqual(rc, 0) self.assertEqual(rc, 0, msg=stderr)
self.assertIn("additionalProperties is required for object schemas", stderr) # The generated parser should reject unknown keys. Verify the key-matching
# helper returns -1 for an unknown key and cbKeyData rejects it.
self.assertIn("int matchKey(Kind k, std::string_view key) const {", stdout)
self.assertNotIn("bool isStrict(Kind k) const", stdout)
def test_additional_properties_false_accepted(self): def test_additional_properties_false_accepted(self):
schema = { schema = {
@@ -287,216 +274,6 @@ class SchemagenAdditionalPropertiesTest(unittest.TestCase):
self.assertEqual(rc, 0, msg=stderr) self.assertEqual(rc, 0, msg=stderr)
class SchemagenCyclicArrayTest(unittest.TestCase):
"""Regression tests for issue #33: cyclic array $ref targets."""
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_direct_self_referential_array_rejected(self):
schema = {
"type": "array",
"items": {"$ref": "#/$defs/Node"},
"$defs": {
"Node": {
"type": "array",
"items": {"$ref": "#/$defs/Node"},
}
},
}
rc, stdout, stderr = self.run_schemagen(schema)
self.assertNotEqual(rc, 0)
self.assertIn("recursive array type is not supported", stderr)
self.assertIn("Node", stderr)
def test_object_field_to_self_referential_array_rejected(self):
schema = {
"type": "object",
"additionalProperties": False,
"properties": {"items": {"$ref": "#/$defs/Items"}},
"$defs": {
"Items": {
"type": "array",
"items": {"$ref": "#/$defs/Items"},
}
},
}
rc, stdout, stderr = self.run_schemagen(schema)
self.assertNotEqual(rc, 0)
self.assertIn("recursive array type is not supported", stderr)
self.assertIn("Items", stderr)
def test_chain_of_array_refs_rejected(self):
schema = {
"type": "object",
"additionalProperties": False,
"properties": {"x": {"$ref": "#/$defs/A"}},
"$defs": {
"A": {"type": "array", "items": {"$ref": "#/$defs/B"}},
"B": {"type": "array", "items": {"$ref": "#/$defs/A"}},
},
}
rc, stdout, stderr = self.run_schemagen(schema)
self.assertNotEqual(rc, 0)
self.assertIn("recursive array type is not supported", stderr)
def test_non_recursive_array_refs_still_allowed(self):
schema = {
"type": "object",
"additionalProperties": False,
"properties": {"roles": {"$ref": "#/$defs/Roles"}},
"$defs": {
"Role": {"enum": ["admin", "user"]},
"Roles": {
"type": "array",
"items": {"$ref": "#/$defs/Role"},
},
},
}
rc, stdout, stderr = self.run_schemagen(schema)
self.assertEqual(rc, 0, msg=stderr)
self.assertIn("std::optional<std::vector<Role>> roles;", stdout)
class SchemagenCyclicNullableObjectTest(unittest.TestCase):
"""Regression tests for issue #14: nullable cyclic $ref targets."""
def setUp(self):
self.repo_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
self.include_dir = os.path.join(self.repo_root, "include")
self.lib_src = os.path.join(self.repo_root, "src", "lib.cpp")
self.compiler = shutil.which("c++")
def _compile_harness(self, tmpdir, schema, harness):
schema_path = os.path.join(tmpdir, "schema.json")
with open(schema_path, "w") as fp:
json.dump(schema, fp)
header_path = os.path.join(tmpdir, "gen.h")
result = subprocess.run(
[
sys.executable,
SCRIPT,
schema_path,
"-o",
header_path,
"--namespace",
"test_schema",
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
lib_obj = os.path.join(tmpdir, "lib.o")
comp_lib = subprocess.run(
[
self.compiler,
"-std=c++20",
"-I",
self.include_dir,
"-I",
os.path.join(self.repo_root, "third_party", "include"),
"-I",
os.path.join(self.repo_root, "third_party", "valgrind"),
"-c",
self.lib_src,
"-o",
lib_obj,
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(comp_lib.returncode, 0, msg=comp_lib.stderr)
cpp_path = os.path.join(tmpdir, "test.cpp")
with open(cpp_path, "w") as fp:
fp.write(harness)
exe_path = os.path.join(tmpdir, "test")
comp = subprocess.run(
[
self.compiler,
"-std=c++20",
"-I",
self.include_dir,
"-I",
tmpdir,
lib_obj,
cpp_path,
"-o",
exe_path,
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(comp.returncode, 0, msg=comp.stderr)
run = subprocess.run([exe_path], capture_output=True, text=True, check=False)
self.assertEqual(run.returncode, 0, msg=run.stdout + run.stderr)
def test_self_referential_nullable_object_accepts_nested_null(self):
"""A nullable object $def must stay nullable on recursive $refs."""
if not self.compiler:
self.skipTest("C++ compiler not available")
schema = {
"type": "object",
"additionalProperties": False,
"properties": {"self": {"$ref": "#/$defs/Self"}},
"$defs": {
"Self": {
"type": ["object", "null"],
"additionalProperties": False,
"properties": {"self": {"$ref": "#/$defs/Self"}},
}
},
}
harness = textwrap.dedent(
"""
#include "gen.h"
#include <cstdio>
#include <cstring>
struct Case { const char *s; WeaselJsonStatus expected; };
int main() {
Case cases[] = {
{ R"({"self": null})", WeaselJson_OK },
{ R"({"self": {"self": null}})", WeaselJson_OK },
{ R"({"self": {"self": {}}})", WeaselJson_OK },
};
for (const auto &c : cases) {
test_schema::RootBuilder b;
char buf[256];
std::strncpy(buf, c.s, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\\0';
WeaselJsonStatus st = b.feed(buf, std::strlen(buf));
st = b.finish();
if (st != c.expected) {
std::printf("case %s expected %d got %d\\n", c.s, c.expected, st);
return 1;
}
}
return 0;
}
"""
)
with tempfile.TemporaryDirectory() as tmpdir:
self._compile_harness(tmpdir, schema, harness)
class SchemagenIntegerBoundaryTest(unittest.TestCase): class SchemagenIntegerBoundaryTest(unittest.TestCase):
"""Regression tests for issue #19: integer slot parsing near int64 boundaries.""" """Regression tests for issue #19: integer slot parsing near int64 boundaries."""
@@ -605,18 +382,9 @@ class SchemagenIntegerBoundaryTest(unittest.TestCase):
("1e-3", "WeaselJson_REJECT", 0), ("1e-3", "WeaselJson_REJECT", 0),
("1000e-3", "WeaselJson_OK", 1), ("1000e-3", "WeaselJson_OK", 1),
("100.0e-2", "WeaselJson_OK", 1), ("100.0e-2", "WeaselJson_OK", 1),
("0.0001e4", "WeaselJson_OK", 1),
("0.001e3", "WeaselJson_OK", 1),
("123.0", "WeaselJson_OK", 123), ("123.0", "WeaselJson_OK", 123),
("9e18", "WeaselJson_OK", 9000000000000000000), ("9e18", "WeaselJson_OK", 9000000000000000000),
("10e18", "WeaselJson_REJECT", 0), ("10e18", "WeaselJson_REJECT", 0),
# Issue #56: zero written with a negative exponent must be accepted.
("0e-1", "WeaselJson_OK", 0),
("0e-2", "WeaselJson_OK", 0),
("0.0e-2", "WeaselJson_OK", 0),
("-0e-2", "WeaselJson_OK", 0),
("0e-20", "WeaselJson_OK", 0),
("0.000e-5", "WeaselJson_OK", 0),
] ]
cases_src = self._build_cases_array("root", cases) cases_src = self._build_cases_array("root", cases)
harness = textwrap.dedent( harness = textwrap.dedent(
@@ -657,11 +425,7 @@ class SchemagenIntegerBoundaryTest(unittest.TestCase):
('{"age":-9223372036854775809}', "WeaselJson_REJECT", 0), ('{"age":-9223372036854775809}', "WeaselJson_REJECT", 0),
('{"age":1e3}', "WeaselJson_OK", 1000), ('{"age":1e3}', "WeaselJson_OK", 1000),
('{"age":2.0}', "WeaselJson_OK", 2), ('{"age":2.0}', "WeaselJson_OK", 2),
('{"age":0.0001e4}', "WeaselJson_OK", 1),
('{"age":0.001}', "WeaselJson_REJECT", 0), ('{"age":0.001}', "WeaselJson_REJECT", 0),
# Issue #56: zero written with a negative exponent must be accepted.
('{"age":0e-2}', "WeaselJson_OK", 0),
('{"age":-0e-20}', "WeaselJson_OK", 0),
( (
'{"age":-9223372036854775808.0}', '{"age":-9223372036854775808.0}',
"WeaselJson_OK", "WeaselJson_OK",
@@ -698,167 +462,12 @@ class SchemagenIntegerBoundaryTest(unittest.TestCase):
) )
schema = { schema = {
"type": "object", "type": "object",
"additionalProperties": False,
"properties": {"age": {"type": "integer"}}, "properties": {"age": {"type": "integer"}},
"required": ["age"], "required": ["age"],
} }
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
self._compile_harness(tmpdir, schema, harness) 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 <cstdio>
#include <string>
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<int>(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)
class SchemagenStringEscapeTest(unittest.TestCase):
"""Regression tests for issue #35: control characters in string literals."""
def setUp(self):
self.repo_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
self.include_dir = os.path.join(self.repo_root, "include")
self.compiler = shutil.which("c++")
def generate_and_compile(self, schema):
"""Run schemagen on schema and syntax-check the resulting header."""
with tempfile.TemporaryDirectory() as tmpdir:
schema_path = os.path.join(tmpdir, "schema.json")
with open(schema_path, "w") as fp:
json.dump(schema, fp)
header_path = os.path.join(tmpdir, "gen.h")
cmd = [
sys.executable,
SCRIPT,
schema_path,
"-o",
header_path,
"--namespace",
"test_schema",
]
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
self.assertEqual(result.returncode, 0, msg=result.stderr)
if self.compiler:
cpp_path = os.path.join(tmpdir, "test.cpp")
with open(cpp_path, "w") as fp:
fp.write(
'#include "gen.h"\n'
"int main() {\n"
" test_schema::RootBuilder b;\n"
" test_schema::Root r = b.take();\n"
" (void)r;\n"
"}\n"
)
comp = subprocess.run(
[
self.compiler,
"-std=c++20",
"-fsyntax-only",
"-I",
self.include_dir,
"-I",
tmpdir,
cpp_path,
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(comp.returncode, 0, msg=comp.stderr)
with open(header_path) as fp:
return fp.read()
def test_newline_in_property_key(self):
"""A JSON key containing a newline must become a valid C++ literal."""
schema = {
"type": "object",
"additionalProperties": False,
"properties": {"a\nb": {"type": "string"}},
}
out = self.generate_and_compile(schema)
self.assertIn('// "a\\nb"', out)
self.assertIn('if (key == "a\\nb") return 0;', out)
def test_newline_in_enum_value(self):
"""An enum value containing a newline must become a valid C++ literal."""
schema = {
"type": "object",
"additionalProperties": False,
"properties": {"x": {"enum": ["a\nb"]}},
}
out = self.generate_and_compile(schema)
self.assertIn('static constexpr const char *X_names[] = { "a\\nb" };', out)
def test_mixed_control_chars_in_enum_value(self):
"""Mixed control characters in an enum value must be escaped."""
schema = {
"type": "object",
"additionalProperties": False,
"properties": {
"x": {"enum": ["x\ny\rz\tw\vq\x00\x01"]},
},
}
out = self.generate_and_compile(schema)
self.assertIn(
'static constexpr const char *X_names[] = { "x\\ny\\rz\\tw\\vq\\u0000\\u0001" };',
out,
)
def test_backslash_and_quote_still_escaped(self):
"""Existing escaping for backslash and double quote must remain correct."""
schema = {
"type": "object",
"additionalProperties": False,
"properties": {'a"b\\c': {"type": "string"}},
}
out = self.generate_and_compile(schema)
self.assertIn('// "a\\"b\\\\c"', out)
self.assertIn('if (key == "a\\"b\\\\c") return 0;', out)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+9 -80
View File
@@ -18,43 +18,6 @@ import keyword
import sys import sys
def _escape_cpp_string(s):
"""Return *s* escaped for use inside a C++ double-quoted string literal.
JSON strings may contain control characters; emitting them verbatim into
generated C++ source breaks tokenization. This helper escapes backslashes
and double quotes, maps common control characters to their short escape
sequences, and uses universal character names (\\u00XX) for any other
character below 0x20.
"""
out = []
for ch in s:
cp = ord(ch)
if cp == 0x09:
out.append("\\t")
elif cp == 0x0A:
out.append("\\n")
elif cp == 0x0B:
out.append("\\v")
elif cp == 0x0C:
out.append("\\f")
elif cp == 0x0D:
out.append("\\r")
elif cp == 0x08:
out.append("\\b")
elif cp == 0x07:
out.append("\\a")
elif cp < 0x20:
out.append(f"\\u{cp:04X}")
elif ch == "\\":
out.append("\\\\")
elif ch == '"':
out.append('\\"')
else:
out.append(ch)
return "".join(out)
class GenError(Exception): class GenError(Exception):
pass pass
@@ -272,24 +235,6 @@ class Builder:
"""Names generated internally that must not collide with user types.""" """Names generated internally that must not collide with user types."""
return name in ("Root", "RootScalar") return name in ("Root", "RootScalar")
@staticmethod
def _array_reaches(target, ty):
"""Return True if `target` can be reached from `ty` by following
TArr element types. This detects self-referential array cycles that
cannot be expressed as C++ structs."""
seen = set()
stack = [ty]
while stack:
cur = stack.pop()
if cur is target:
return True
if id(cur) in seen:
continue
seen.add(id(cur))
if isinstance(cur, TArr):
stack.append(cur.elem)
return False
def ref_name(self, ref): def ref_name(self, ref):
if not ref.startswith("#/"): if not ref.startswith("#/"):
raise GenError(f"only local $ref supported, got: {ref}") raise GenError(f"only local $ref supported, got: {ref}")
@@ -377,12 +322,6 @@ class Builder:
) )
t.elem = elem t.elem = elem
t.elem_nullable = elem_nullable t.elem_nullable = elem_nullable
# Self-referential array cycles (directly or through a chain of
# array definitions) cannot be represented as a C++ value type.
if self._array_reaches(t, elem):
raise GenError(
"recursive array type is not supported: " f"{defname or hint!r}"
)
return result return result
scalar = { scalar = {
@@ -417,13 +356,7 @@ class Builder:
tobj = TObj(name) tobj = TObj(name)
if defname is not None: if defname is not None:
self._building[defname] = (tobj, nullable) self._building[defname] = (tobj, nullable)
ap = node.get("additionalProperties", None) ap = node.get("additionalProperties", False)
if ap is None:
raise GenError(
"additionalProperties is required for object schemas; set it "
"explicitly to false to reject unknown keys (absent "
"additionalProperties is not supported)"
)
if ap is True: if ap is True:
raise GenError("additionalProperties: true is not supported") raise GenError("additionalProperties: true is not supported")
if isinstance(ap, dict): if isinstance(ap, dict):
@@ -685,8 +618,7 @@ namespace {ns} {{"""
and f.ty.kind in ("int", "dbl", "bool") and f.ty.kind in ("int", "dbl", "bool")
): ):
init = " = 0" if f.ty.kind != "bool" else " = false" init = " = 0" if f.ty.kind != "bool" else " = false"
esc_key = _escape_cpp_string(f.key) out.append(f' {store} {f.cpp}{init}; // "{f.key}"')
out.append(f' {store} {f.cpp}{init}; // "{esc_key}"')
out.append("};") out.append("};")
out.append("") out.append("")
return "\n".join(out) return "\n".join(out)
@@ -814,7 +746,7 @@ namespace {ns} {{"""
for name, obj in self.b.objects.items(): for name, obj in self.b.objects.items():
lines.append(f" case Kind::{name}:") lines.append(f" case Kind::{name}:")
for i, fld in enumerate(obj.fields): for i, fld in enumerate(obj.fields):
esc = _escape_cpp_string(fld.key) esc = fld.key.replace("\\", "\\\\").replace('"', '\\"')
lines.append(f' if (key == "{esc}") return {i};') lines.append(f' if (key == "{esc}") return {i};')
lines.append(" return -1;") lines.append(" return -1;")
lines.append(" default: return -1;") lines.append(" default: return -1;")
@@ -897,7 +829,10 @@ namespace {ns} {{"""
def _enum_name_arrays(self): def _enum_name_arrays(self):
out = [] out = []
for e in self.b.enums.values(): for e in self.b.enums.values():
lits = ", ".join('"' + _escape_cpp_string(v) + '"' for v in e.values) lits = ", ".join(
'"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"'
for v in e.values
)
out.append( out.append(
f" static constexpr const char *{e.name}_names[] = {{ {lits} }};" f" static constexpr const char *{e.name}_names[] = {{ {lits} }};"
) )
@@ -957,10 +892,6 @@ public:
explicit RootBuilder(int stackSize = 1024) {{ explicit RootBuilder(int stackSize = 1024) {{
cb_ = makeCallbacks(); cb_ = makeCallbacks();
parser_ = WeaselJsonParser_create(stackSize, &cb_, this, 0); parser_ = WeaselJsonParser_create(stackSize, &cb_, this, 0);
if (!parser_) {{
error_ = true;
return;
}}
{self._ctor_body()} {self._ctor_body()}
}} }}
~RootBuilder() {{ if (parser_) WeaselJsonParser_destroy(parser_); }} ~RootBuilder() {{ if (parser_) WeaselJsonParser_destroy(parser_); }}
@@ -1064,11 +995,9 @@ private:
++trim; ++trim;
}} }}
int64_t finalExp = exp - fracDigits + trim; int64_t finalExp = exp - fracDigits + trim;
size_t leadingZeros = 0;
while (leadingZeros < digits.size() && digits[leadingZeros] == '0') ++leadingZeros;
if (leadingZeros == digits.size()) {{ out = 0; return true; }}
if (finalExp < 0) return false; if (finalExp < 0) return false;
if (leadingZeros > 0) digits.erase(0, leadingZeros); while (!digits.empty() && digits.front() == '0') digits.erase(digits.begin());
if (digits.empty()) {{ out = 0; return true; }}
constexpr uint64_t kMaxNeg = 9223372036854775808ULL; constexpr uint64_t kMaxNeg = 9223372036854775808ULL;
constexpr uint64_t kMaxPos = 9223372036854775807ULL; constexpr uint64_t kMaxPos = 9223372036854775807ULL;
+1 -3
View File
@@ -38,8 +38,6 @@ enum WeaselJsonStatus {
WeaselJson_REJECT, WeaselJson_REJECT,
/** json is too deeply nested */ /** json is too deeply nested */
WeaselJson_OVERFLOW, WeaselJson_OVERFLOW,
/** Tried to call parse on a null parser */
WeaselJson_NULL,
}; };
typedef struct WeaselJsonParser WeaselJsonParser; typedef struct WeaselJsonParser WeaselJsonParser;
@@ -67,7 +65,7 @@ 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` must not be negative; a negative length is treated as a * `len` is 0. `len` must not be negative; a negative length is treated as a
* rejected input. Returns WeaselJson_NULL if parser is null */ * rejected input. */
WeaselJsonStatus WeaselJsonParser_parse(WeaselJsonParser *parser, char *buf, WeaselJsonStatus WeaselJsonParser_parse(WeaselJsonParser *parser, char *buf,
int len); int len);
-1
View File
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <map> #include <map>
#include <memory> #include <memory>
#include <optional> #include <optional>
-9
View File
@@ -29,26 +29,17 @@ WeaselJsonParser_create(int stackSize, const WeaselJsonCallbacks *callbacks,
__attribute__((visibility("default"))) void __attribute__((visibility("default"))) void
WeaselJsonParser_reset(WeaselJsonParser *parser) { WeaselJsonParser_reset(WeaselJsonParser *parser) {
if (parser == nullptr) {
return;
}
((Parser3 *)parser)->reset(); ((Parser3 *)parser)->reset();
} }
__attribute__((visibility("default"))) void __attribute__((visibility("default"))) void
WeaselJsonParser_destroy(WeaselJsonParser *parser) { WeaselJsonParser_destroy(WeaselJsonParser *parser) {
if (parser == nullptr) {
return;
}
((Parser3 *)parser)->~Parser3(); ((Parser3 *)parser)->~Parser3();
free(parser); free(parser);
} }
__attribute__((visibility("default"))) WeaselJsonStatus __attribute__((visibility("default"))) WeaselJsonStatus
WeaselJsonParser_parse(WeaselJsonParser *parser, char *buf, int len) { WeaselJsonParser_parse(WeaselJsonParser *parser, char *buf, int len) {
if (parser == nullptr) [[unlikely]] {
return WeaselJson_NULL;
}
return ((Parser3 *)parser)->parse(buf, len); return ((Parser3 *)parser)->parse(buf, len);
} }
} }
+18 -16
View File
@@ -83,26 +83,28 @@ struct Parser3 {
[[nodiscard]] WeaselJsonStatus parse(char *buf, int len); [[nodiscard]] WeaselJsonStatus parse(char *buf, int len);
void flushNumber(bool done, char *buf) { void flushNumber(bool done, char *buf) {
int len = buf - dataBegin; int len = (intptr_t)buf - (intptr_t)dataBegin;
assert(len >= 0); assert(len >= 0);
if (done || len > 0) { if (done || len > 0) {
callbacks->on_number_data(userdata, dataBegin, len, done); callbacks->on_number_data(userdata, dataBegin ? dataBegin : "", len,
done);
} }
} }
void flushString(bool done, char *buf) { void flushString(bool done, char *buf) {
int len; int len;
if (!(flags & WeaselJsonRaw)) { if (!(flags & WeaselJsonRaw)) {
len = writeBuf - dataBegin; len = (intptr_t)writeBuf - (intptr_t)dataBegin;
} else { } else {
len = buf - dataBegin; len = (intptr_t)buf - (intptr_t)dataBegin;
} }
assert(len >= 0); assert(len >= 0);
if (done || len > 0) { if (done || len > 0) {
const char *data = dataBegin ? dataBegin : "";
if (inKey) { if (inKey) {
callbacks->on_key_data(userdata, dataBegin, len, done); callbacks->on_key_data(userdata, data, len, done);
} else { } else {
callbacks->on_string_data(userdata, dataBegin, len, done); callbacks->on_string_data(userdata, data, len, done);
} }
} }
} }
@@ -139,7 +141,7 @@ struct Parser3 {
stackPtr = stack(); stackPtr = stack();
std::ignore = push({N_VALUE, N_WHITESPACE, T_EOF}); std::ignore = push({N_VALUE, N_WHITESPACE, T_EOF});
inKey = false; inKey = false;
terminalStatus = WeaselJson_OK; rejected = false;
utf8Codepoint = 0; utf8Codepoint = 0;
utf16Surrogate = 0; utf16Surrogate = 0;
minCodepoint = 0; minCodepoint = 0;
@@ -162,7 +164,7 @@ struct Parser3 {
NumDfa numDfa; NumDfa numDfa;
Utf8Dfa strDfa; Utf8Dfa strDfa;
bool inKey = false; bool inKey = false;
WeaselJsonStatus terminalStatus = WeaselJson_OK; bool rejected = false;
#ifndef HAS_MUSTTAIL #ifndef HAS_MUSTTAIL
char *stashBufForTrampoline; char *stashBufForTrampoline;
@@ -650,7 +652,7 @@ inline PRESERVE_NONE ContinuationStatus n_string2(Parser3 *self, char *buf,
self->writeBuf[0] = (0b00000111 & codepoint) | 0b11110000; self->writeBuf[0] = (0b00000111 & codepoint) | 0b11110000;
self->writeBuf += 4; self->writeBuf += 4;
} }
} else if (0xdc00 <= codepoint && codepoint <= 0xdfff) [[unlikely]] { } else if (0xdc00 <= codepoint && codepoint <= 0xdfff) {
return WeaselJson_REJECT; return WeaselJson_REJECT;
} else { } else {
if (!(self->flags & WeaselJsonRaw)) { if (!(self->flags & WeaselJsonRaw)) {
@@ -1070,12 +1072,12 @@ 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->terminalStatus != WeaselJson_OK) [[unlikely]] { if (this->rejected) [[unlikely]] {
return this->terminalStatus; return WeaselJson_REJECT;
} }
if (len < 0) [[unlikely]] { if (len < 0) [[unlikely]] {
this->terminalStatus = WeaselJson_REJECT; this->rejected = true;
return WeaselJson_REJECT; return WeaselJson_REJECT;
} }
@@ -1085,8 +1087,8 @@ inline WeaselJsonStatus Parser3::parse(char *buf, int len) {
// range. // range.
ContinuationStatus status = ContinuationStatus status =
symbolTables.continuations[top()](this, buf, buf + len); symbolTables.continuations[top()](this, buf, buf + len);
if (status > WeaselJson_AGAIN) { if (status == WeaselJson_REJECT) {
this->terminalStatus = WeaselJsonStatus(status); this->rejected = true;
} }
return WeaselJsonStatus(status); return WeaselJsonStatus(status);
#else #else
@@ -1095,8 +1097,8 @@ inline WeaselJsonStatus Parser3::parse(char *buf, int len) {
while ((result = symbolTables.continuations[top()]( while ((result = symbolTables.continuations[top()](
this, stashBufForTrampoline, buf + len)) == kBounce) this, stashBufForTrampoline, buf + len)) == kBounce)
; ;
if (result > WeaselJson_AGAIN) { if (result == WeaselJson_REJECT) {
this->terminalStatus = WeaselJsonStatus(result); this->rejected = true;
} }
return WeaselJsonStatus(result); return WeaselJsonStatus(result);
#endif #endif
+18 -74
View File
@@ -202,62 +202,6 @@ TEST_CASE("parser3") {
} }
} }
TEST_CASE("overflow state is sticky") {
auto c = noopCallbacks();
// stackSize 3 is exactly big enough to hold reset()'s bootstrap, but too
// small for nested arrays. Overflows must be terminal like rejects: a later
// end-of-data call must never report OK for an incomplete document.
auto *parser = WeaselJsonParser_create(3, &c, nullptr, 0);
REQUIRE(parser != nullptr);
std::string doc = "[[";
REQUIRE(WeaselJsonParser_parse(parser, doc.data(), doc.size()) ==
WeaselJson_OVERFLOW);
// After overflow, the end-of-data call must not return OK.
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) != WeaselJson_OK);
// It should keep reporting a terminal failure.
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) != WeaselJson_OK);
// Further data chunks must also stay terminal.
std::string more = "]]";
REQUIRE(WeaselJsonParser_parse(parser, more.data(), more.size()) !=
WeaselJson_OK);
WeaselJsonParser_destroy(parser);
}
TEST_CASE("overflow is sticky for nested objects") {
auto c = noopCallbacks();
auto *parser = WeaselJsonParser_create(4, &c, nullptr, 0);
REQUIRE(parser != nullptr);
std::string doc = "{\"a\":{ \"a\":";
REQUIRE(WeaselJsonParser_parse(parser, doc.data(), doc.size()) ==
WeaselJson_OVERFLOW);
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) != WeaselJson_OK);
WeaselJsonParser_destroy(parser);
}
TEST_CASE("reset clears overflow state") {
auto c = noopCallbacks();
auto *parser = WeaselJsonParser_create(3, &c, nullptr, 0);
REQUIRE(parser != nullptr);
std::string doc = "[[";
REQUIRE(WeaselJsonParser_parse(parser, doc.data(), doc.size()) ==
WeaselJson_OVERFLOW);
// After reset the parser should accept a minimal document again.
WeaselJsonParser_reset(parser);
std::string copy = "1";
REQUIRE(WeaselJsonParser_parse(parser, copy.data(), copy.size()) ==
WeaselJson_AGAIN);
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) == WeaselJson_OK);
WeaselJsonParser_destroy(parser);
}
TEST_CASE("rejected state is sticky") { TEST_CASE("rejected state is sticky") {
auto c = noopCallbacks(); auto c = noopCallbacks();
auto *parser = WeaselJsonParser_create(1024, &c, nullptr, 0); auto *parser = WeaselJsonParser_create(1024, &c, nullptr, 0);
@@ -302,20 +246,6 @@ TEST_CASE("create rejects too-small stack") {
WeaselJsonParser_destroy(parser); WeaselJsonParser_destroy(parser);
} }
TEST_CASE("reset and destroy accept null parser") {
// Creation can legitimately fail and return null. The cleanup functions must
// tolerate a null pointer the same way free(nullptr) is a no-op.
auto c = noopCallbacks();
WeaselJsonParser *parser = WeaselJsonParser_create(-1, &c, nullptr, 0);
REQUIRE(parser == nullptr);
WeaselJsonParser_reset(parser); // must not crash
WeaselJsonParser_destroy(parser); // must not crash
// Calling reset/destroy on literal nullptr directly must also be safe.
WeaselJsonParser_reset(nullptr);
WeaselJsonParser_destroy(nullptr);
}
TEST_CASE("parse rejects negative length") { TEST_CASE("parse rejects negative length") {
auto c = noopCallbacks(); auto c = noopCallbacks();
auto *parser = WeaselJsonParser_create(1024, &c, nullptr, 0); auto *parser = WeaselJsonParser_create(1024, &c, nullptr, 0);
@@ -330,10 +260,6 @@ TEST_CASE("parse rejects negative length") {
WeaselJsonParser_destroy(parser); WeaselJsonParser_destroy(parser);
} }
TEST_CASE("Calling parse with nullptr doesn't crash") {
REQUIRE(WeaselJsonParser_parse(nullptr, nullptr, 0) == WeaselJson_NULL);
}
TEST_CASE("streaming") { testStreaming(json); } TEST_CASE("streaming") { testStreaming(json); }
TEST_CASE("reset clears inKey and transient state") { TEST_CASE("reset clears inKey and transient state") {
@@ -377,6 +303,24 @@ TEST_CASE("reset clears inKey and transient state") {
WeaselJsonParser_destroy(parser); WeaselJsonParser_destroy(parser);
} }
TEST_CASE("scalar ending at chunk boundary is finalized at EOF") {
// A number whose digits exactly fill the first chunk must not invoke
// undefined behaviour on the EOF call, and must still signal completion.
auto c = serializeCallbacks();
SerializeState state;
auto *parser = WeaselJsonParser_create(1024, &c, &state, 0);
REQUIRE(parser != nullptr);
std::string chunk = "123";
REQUIRE(WeaselJsonParser_parse(parser, chunk.data(), chunk.size()) ==
WeaselJson_AGAIN);
REQUIRE(WeaselJsonParser_parse(parser, nullptr, 0) == WeaselJson_OK);
CHECK(state.result == "(123)");
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);
-3
View File
@@ -33,9 +33,6 @@ int main(int argc, char **argv) {
case WeaselJson_REJECT: case WeaselJson_REJECT:
case WeaselJson_OVERFLOW: case WeaselJson_OVERFLOW:
return 1; return 1;
case WeaselJson_NULL:
fprintf(stderr, "parse called with a null parser\n");
return 1;
} }
if (l == 0) { if (l == 0) {
return 1; return 1;
-12
View File
@@ -95,20 +95,8 @@ def test_create_rejects_too_small_stack():
raise AssertionError(f"expected ValueError for stackSize={stack_size}") raise AssertionError(f"expected ValueError for stackSize={stack_size}")
def test_missing_library_raises_oserror():
try:
weaseljson.WeaselJsonParser(
weaseljson.WeaselJsonCallbacksBase(),
build_dir="/nonexistent",
)
except OSError:
return
raise AssertionError("expected OSError when the shared library is missing")
if __name__ == "__main__": if __name__ == "__main__":
test_object_keys_routed_correctly() test_object_keys_routed_correctly()
test_mixed_values() test_mixed_values()
test_create_rejects_too_small_stack() test_create_rejects_too_small_stack()
test_missing_library_raises_oserror()
print("python bindings ok") print("python bindings ok")
+7 -2
View File
@@ -30,7 +30,6 @@ class WeaselJsonStatus(enum.Enum):
AGAIN = 1 AGAIN = 1
REJECT = 2 REJECT = 2
OVERFLOW = 3 OVERFLOW = 3
NULL = 4
class WeaselJsonCallbacksBase: class WeaselJsonCallbacksBase:
@@ -85,7 +84,13 @@ class WeaselJsonParser:
pass pass
if self._lib is None: if self._lib is None:
raise OSError(f"Could not load libweaseljson from {build_dir}") import sys
print(
"Could not find libweaseljson implementation",
file=sys.stderr,
)
sys.exit(1)
self._lib.WeaselJsonParser_create.argtypes = ( self._lib.WeaselJsonParser_create.argtypes = (
ctypes.c_int, ctypes.c_int,