From 8dea480c9f065fdac76f848ed9ec7a07fd1e9870 Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Sat, 22 Jan 2022 14:33:26 -0500 Subject: [PATCH] Allow optional fields in json "schema" checks --- ChangeLog | 4 +++ include/qpdf/JSON.hh | 23 +++++++++++++-- libqpdf/JSON.cc | 54 +++++++++++++++++++++++++----------- libtests/json.cc | 53 +++++++++++++++++++++++++++-------- libtests/libtests.testcov | 2 ++ libtests/qtest/json/json.out | 6 ++++ 6 files changed, 113 insertions(+), 29 deletions(-) diff --git a/ChangeLog b/ChangeLog index b70d6699..2433a76a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,9 @@ 2022-01-22 Jay Berkenbilt + * JSON: for (qpdf-specific, not official) "schema" checking, add + the ability to treat missing fields as optional. Also ensure that + values in the schema are dictionary, array, or string. + * Add convenience methods isNameAndEquals and isDictionaryOfType to QPDFObjectHandle with corresponding functions added to the C API. Thanks to m-holger for the contribution. diff --git a/include/qpdf/JSON.hh b/include/qpdf/JSON.hh index 3b13b4fe..d45fb3e1 100644 --- a/include/qpdf/JSON.hh +++ b/include/qpdf/JSON.hh @@ -107,21 +107,39 @@ class JSON // single-element arrays, and strings only. // * Recursively walk the schema // * If the current value is a dictionary, this object must have - // a dictionary in the same place with the same keys + // a dictionary in the same place with the same keys. If flags + // contains f_optional, a key in the schema does not have to + // be present in the object. Otherwise, all keys have to be + // present. Any key in the object must be present in the + // schema. // * If the current value is an array, this object must have an // array in the same place. The schema's array must contain a // single element, which is used as a schema to validate each // element of this object's corresponding array. - // * Otherwise, the value is ignored. + // * Otherwise, the value must be a string whose value is a + // description of the object's corresponding value, which may + // have any type. // // QPDF's JSON output conforms to certain strict compatibility // rules as discussed in the manual. The idea is that a JSON // structure created manually in qpdf.cc doubles as both JSON help // information and a schema for validating the JSON that qpdf // generates. Any discrepancies are a bug in qpdf. + // + // Flags is a bitwise or of values from check_flags_e. + enum check_flags_e { + f_none = 0, + f_optional = 1 << 0, + }; + QPDF_DLL + bool checkSchema(JSON schema, unsigned long flags, + std::list& errors); + + // Same as passing 0 for flags QPDF_DLL bool checkSchema(JSON schema, std::list& errors); + // Create a JSON object from a string. QPDF_DLL static JSON parse(std::string const&); @@ -180,6 +198,7 @@ class JSON static bool checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, + unsigned long flags, std::list& errors, std::string prefix); diff --git a/libqpdf/JSON.cc b/libqpdf/JSON.cc index af98553e..f70145a7 100644 --- a/libqpdf/JSON.cc +++ b/libqpdf/JSON.cc @@ -394,12 +394,21 @@ JSON::checkSchema(JSON schema, std::list& errors) { return checkSchemaInternal(this->m->value.getPointer(), schema.m->value.getPointer(), - errors, ""); + 0, errors, ""); } +bool +JSON::checkSchema(JSON schema, unsigned long flags, + std::list& errors) +{ + return checkSchemaInternal(this->m->value.getPointer(), + schema.m->value.getPointer(), + flags, errors, ""); +} bool JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, + unsigned long flags, std::list& errors, std::string prefix) { @@ -409,6 +418,8 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, JSON_array* sch_arr = dynamic_cast(sch_v); JSON_dictionary* sch_dict = dynamic_cast(sch_v); + JSON_string* sch_str = dynamic_cast(sch_v); + std::string err_prefix; if (prefix.empty()) { @@ -446,34 +457,38 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, { std::string const& key = iter.first; checkSchemaInternal( - this_dict->members[key].getPointer(), - pattern_schema, - errors, prefix + "." + key); + this_dict->members[key].getPointer(), pattern_schema, + flags, errors, prefix + "." + key); } } else if (sch_dict) { - for (std::map >::iterator iter = - sch_dict->members.begin(); - iter != sch_dict->members.end(); ++iter) + for (auto& iter: sch_dict->members) { - std::string const& key = (*iter).first; + std::string const& key = iter.first; if (this_dict->members.count(key)) { checkSchemaInternal( this_dict->members[key].getPointer(), - (*iter).second.getPointer(), - errors, prefix + "." + key); + iter.second.getPointer(), + flags, errors, prefix + "." + key); } else { - QTC::TC("libtests", "JSON key missing in object"); - errors.push_back( - err_prefix + ": key \"" + key + - "\" is present in schema but missing in object"); + if (flags & f_optional) + { + QTC::TC("libtests", "JSON optional key"); + } + else + { + QTC::TC("libtests", "JSON key missing in object"); + errors.push_back( + err_prefix + ": key \"" + key + + "\" is present in schema but missing in object"); + } } } - for (std::map >::iterator iter = + for (std::map>::iterator iter = this_dict->members.begin(); iter != this_dict->members.end(); ++iter) { @@ -510,9 +525,16 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, checkSchemaInternal( (*iter).getPointer(), sch_arr->elements.at(0).getPointer(), - errors, prefix + "." + QUtil::int_to_string(i)); + flags, errors, prefix + "." + QUtil::int_to_string(i)); } } + else if (! sch_str) + { + QTC::TC("libtests", "JSON schema other type"); + errors.push_back(err_prefix + + " schema value is not dictionary, array, or string"); + return false; + } return errors.empty(); } diff --git a/libtests/json.cc b/libtests/json.cc index 006f00cf..754c2b3e 100644 --- a/libtests/json.cc +++ b/libtests/json.cc @@ -112,12 +112,12 @@ static void test_main() assert(dvalue == xdvalue); } -static void check_schema(JSON& obj, JSON& schema, bool exp, - std::string const& description) +static void check_schema(JSON& obj, JSON& schema, unsigned long flags, + bool exp, std::string const& description) { std::list errors; std::cout << "--- " << description << std::endl; - assert(exp == obj.checkSchema(schema, errors)); + assert(exp == obj.checkSchema(schema, flags, errors)); for (std::list::iterator iter = errors.begin(); iter != errors.end(); ++iter) { @@ -134,8 +134,7 @@ static void test_schema() "a": { "q": "queue", "r": { - "x": "ecks", - "y": "(bool) why" + "x": "ecks" }, "s": [ "esses" @@ -151,14 +150,14 @@ static void test_schema() "three": { "": { "z": "ebra", - "o": "(optional, string) optional" + "o": "ptional" } } } )"); JSON a = JSON::parse(R"(["not a", "dictionary"])"); - check_schema(a, schema, false, "top-level type mismatch"); + check_schema(a, schema, 0, false, "top-level type mismatch"); JSON b = JSON::parse(R"( { "one": { @@ -205,10 +204,42 @@ static void test_schema() } )"); - check_schema(b, schema, false, "missing items"); - check_schema(a, a, false, "top-level schema array error"); - check_schema(b, b, false, "lower-level schema array error"); - check_schema(schema, schema, true, "pass"); + check_schema(b, schema, 0, false, "missing items"); + check_schema(a, a, 0, false, "top-level schema array error"); + check_schema(b, b, 0, false, "lower-level schema array error"); + + JSON bad_schema = JSON::parse(R"({"a": true, "b": "potato?"})"); + check_schema(bad_schema, bad_schema, 0, false, "bad schema field type"); + + JSON good = JSON::parse(R"( +{ + "one": { + "a": { + "q": "potato", + "r": { + "x": [1, null] + }, + "s": [ + null, + "anything" + ] + } + }, + "two": [ + { + "glarp": "enspliel", + "goose": 3.14 + } + ], + "three": { + "": { + "z": "ebra" + } + } +} +)"); + check_schema(good, schema, 0, false, "not optional"); + check_schema(good, schema, JSON::f_optional, true, "pass"); } int main() diff --git a/libtests/libtests.testcov b/libtests/libtests.testcov index 6284c0e8..907e6579 100644 --- a/libtests/libtests.testcov +++ b/libtests/libtests.testcov @@ -89,3 +89,5 @@ JSON parse premature end of u 0 JSON parse bad hex after u 0 JSONHandler unhandled value 0 JSONHandler unexpected key 0 +JSON schema other type 0 +JSON optional key 0 diff --git a/libtests/qtest/json/json.out b/libtests/qtest/json/json.out index 1a320dcf..c7cb85a7 100644 --- a/libtests/qtest/json/json.out +++ b/libtests/qtest/json/json.out @@ -21,6 +21,12 @@ top-level object schema array contains other than one item json key ".one.a.r" schema array contains other than one item json key ".two" schema array contains other than one item --- +--- bad schema field type +json key ".a" schema value is not dictionary, array, or string +--- +--- not optional +json key ".three.": key "o" is present in schema but missing in object +--- --- pass --- end of json tests