2
1
mirror of https://github.com/qpdf/qpdf.git synced 2024-05-31 01:10:51 +00:00

Allow optional fields in json "schema" checks

This commit is contained in:
Jay Berkenbilt 2022-01-22 14:33:26 -05:00
parent 558ba2823e
commit 8dea480c9f
6 changed files with 113 additions and 29 deletions

View File

@ -1,5 +1,9 @@
2022-01-22 Jay Berkenbilt <ejb@ql.org> 2022-01-22 Jay Berkenbilt <ejb@ql.org>
* 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 * Add convenience methods isNameAndEquals and isDictionaryOfType
to QPDFObjectHandle with corresponding functions added to the C to QPDFObjectHandle with corresponding functions added to the C
API. Thanks to m-holger for the contribution. API. Thanks to m-holger for the contribution.

View File

@ -107,21 +107,39 @@ class JSON
// single-element arrays, and strings only. // single-element arrays, and strings only.
// * Recursively walk the schema // * Recursively walk the schema
// * If the current value is a dictionary, this object must have // * 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 // * If the current value is an array, this object must have an
// array in the same place. The schema's array must contain a // array in the same place. The schema's array must contain a
// single element, which is used as a schema to validate each // single element, which is used as a schema to validate each
// element of this object's corresponding array. // 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 // QPDF's JSON output conforms to certain strict compatibility
// rules as discussed in the manual. The idea is that a JSON // rules as discussed in the manual. The idea is that a JSON
// structure created manually in qpdf.cc doubles as both JSON help // structure created manually in qpdf.cc doubles as both JSON help
// information and a schema for validating the JSON that qpdf // information and a schema for validating the JSON that qpdf
// generates. Any discrepancies are a bug in 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<std::string>& errors);
// Same as passing 0 for flags
QPDF_DLL QPDF_DLL
bool checkSchema(JSON schema, std::list<std::string>& errors); bool checkSchema(JSON schema, std::list<std::string>& errors);
// Create a JSON object from a string. // Create a JSON object from a string.
QPDF_DLL QPDF_DLL
static JSON parse(std::string const&); static JSON parse(std::string const&);
@ -180,6 +198,7 @@ class JSON
static bool static bool
checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
unsigned long flags,
std::list<std::string>& errors, std::list<std::string>& errors,
std::string prefix); std::string prefix);

View File

@ -394,12 +394,21 @@ JSON::checkSchema(JSON schema, std::list<std::string>& errors)
{ {
return checkSchemaInternal(this->m->value.getPointer(), return checkSchemaInternal(this->m->value.getPointer(),
schema.m->value.getPointer(), schema.m->value.getPointer(),
errors, ""); 0, errors, "");
} }
bool
JSON::checkSchema(JSON schema, unsigned long flags,
std::list<std::string>& errors)
{
return checkSchemaInternal(this->m->value.getPointer(),
schema.m->value.getPointer(),
flags, errors, "");
}
bool bool
JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
unsigned long flags,
std::list<std::string>& errors, std::list<std::string>& errors,
std::string prefix) std::string prefix)
{ {
@ -409,6 +418,8 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
JSON_array* sch_arr = dynamic_cast<JSON_array*>(sch_v); JSON_array* sch_arr = dynamic_cast<JSON_array*>(sch_v);
JSON_dictionary* sch_dict = dynamic_cast<JSON_dictionary*>(sch_v); JSON_dictionary* sch_dict = dynamic_cast<JSON_dictionary*>(sch_v);
JSON_string* sch_str = dynamic_cast<JSON_string*>(sch_v);
std::string err_prefix; std::string err_prefix;
if (prefix.empty()) if (prefix.empty())
{ {
@ -446,34 +457,38 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
{ {
std::string const& key = iter.first; std::string const& key = iter.first;
checkSchemaInternal( checkSchemaInternal(
this_dict->members[key].getPointer(), this_dict->members[key].getPointer(), pattern_schema,
pattern_schema, flags, errors, prefix + "." + key);
errors, prefix + "." + key);
} }
} }
else if (sch_dict) else if (sch_dict)
{ {
for (std::map<std::string, PointerHolder<JSON_value> >::iterator iter = for (auto& iter: sch_dict->members)
sch_dict->members.begin();
iter != sch_dict->members.end(); ++iter)
{ {
std::string const& key = (*iter).first; std::string const& key = iter.first;
if (this_dict->members.count(key)) if (this_dict->members.count(key))
{ {
checkSchemaInternal( checkSchemaInternal(
this_dict->members[key].getPointer(), this_dict->members[key].getPointer(),
(*iter).second.getPointer(), iter.second.getPointer(),
errors, prefix + "." + key); flags, errors, prefix + "." + key);
} }
else else
{ {
QTC::TC("libtests", "JSON key missing in object"); if (flags & f_optional)
errors.push_back( {
err_prefix + ": key \"" + key + QTC::TC("libtests", "JSON optional key");
"\" is present in schema but missing in object"); }
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<std::string, PointerHolder<JSON_value> >::iterator iter = for (std::map<std::string, PointerHolder<JSON_value>>::iterator iter =
this_dict->members.begin(); this_dict->members.begin();
iter != this_dict->members.end(); ++iter) iter != this_dict->members.end(); ++iter)
{ {
@ -510,9 +525,16 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
checkSchemaInternal( checkSchemaInternal(
(*iter).getPointer(), (*iter).getPointer(),
sch_arr->elements.at(0).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(); return errors.empty();
} }

View File

@ -112,12 +112,12 @@ static void test_main()
assert(dvalue == xdvalue); assert(dvalue == xdvalue);
} }
static void check_schema(JSON& obj, JSON& schema, bool exp, static void check_schema(JSON& obj, JSON& schema, unsigned long flags,
std::string const& description) bool exp, std::string const& description)
{ {
std::list<std::string> errors; std::list<std::string> errors;
std::cout << "--- " << description << std::endl; std::cout << "--- " << description << std::endl;
assert(exp == obj.checkSchema(schema, errors)); assert(exp == obj.checkSchema(schema, flags, errors));
for (std::list<std::string>::iterator iter = errors.begin(); for (std::list<std::string>::iterator iter = errors.begin();
iter != errors.end(); ++iter) iter != errors.end(); ++iter)
{ {
@ -134,8 +134,7 @@ static void test_schema()
"a": { "a": {
"q": "queue", "q": "queue",
"r": { "r": {
"x": "ecks", "x": "ecks"
"y": "(bool) why"
}, },
"s": [ "s": [
"esses" "esses"
@ -151,14 +150,14 @@ static void test_schema()
"three": { "three": {
"<objid>": { "<objid>": {
"z": "ebra", "z": "ebra",
"o": "(optional, string) optional" "o": "ptional"
} }
} }
} }
)"); )");
JSON a = JSON::parse(R"(["not a", "dictionary"])"); 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"( JSON b = JSON::parse(R"(
{ {
"one": { "one": {
@ -205,10 +204,42 @@ static void test_schema()
} }
)"); )");
check_schema(b, schema, false, "missing items"); check_schema(b, schema, 0, false, "missing items");
check_schema(a, a, false, "top-level schema array error"); check_schema(a, a, 0, false, "top-level schema array error");
check_schema(b, b, false, "lower-level schema array error"); check_schema(b, b, 0, false, "lower-level schema array error");
check_schema(schema, schema, true, "pass");
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": {
"<objid>": {
"z": "ebra"
}
}
}
)");
check_schema(good, schema, 0, false, "not optional");
check_schema(good, schema, JSON::f_optional, true, "pass");
} }
int main() int main()

View File

@ -89,3 +89,5 @@ JSON parse premature end of u 0
JSON parse bad hex after u 0 JSON parse bad hex after u 0
JSONHandler unhandled value 0 JSONHandler unhandled value 0
JSONHandler unexpected key 0 JSONHandler unexpected key 0
JSON schema other type 0
JSON optional key 0

View File

@ -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 ".one.a.r" schema array contains other than one item
json key ".two" 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.<objid>": key "o" is present in schema but missing in object
---
--- pass --- pass
--- ---
end of json tests end of json tests