schemagen: escape control characters in generated C++ string literals
CI / pre-commit (pull_request) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (pull_request) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (pull_request) Successful in 1m32s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (pull_request) Successful in 52s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (pull_request) Successful in 1m28s
CI / pre-commit (pull_request) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-arm64, ubuntu-latest-arm64, true) (pull_request) Successful in 53s
CI / build (-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++, clang-amd64, ubuntu-latest-amd64, true) (pull_request) Successful in 1m32s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-arm64, ubuntu-latest-arm64, false) (pull_request) Successful in 52s
CI / build (-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++, gcc-amd64, ubuntu-latest-amd64, false) (pull_request) Successful in 1m28s
Fixes #35. Add a helper to escape C++ string literals so that JSON control characters (\n, \r, \t, and other bytes below 0x20) are emitted as escape sequences instead of raw bytes. Use it for: - field comments that include the JSON property key - object key comparison literals in matchKey() - enum name arrays Also add regression tests that generate and syntax-check headers for schemas containing newlines and other control characters in property keys and enum values.
This commit is contained in:
@@ -518,5 +518,100 @@ class SchemagenIntegerBoundaryTest(unittest.TestCase):
|
||||
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", "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", "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",
|
||||
"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", "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__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user