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>
* 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.

View File

@ -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<std::string>& errors);
// Same as passing 0 for flags
QPDF_DLL
bool checkSchema(JSON schema, std::list<std::string>& 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<std::string>& errors,
std::string prefix);

View File

@ -394,12 +394,21 @@ JSON::checkSchema(JSON schema, std::list<std::string>& errors)
{
return checkSchemaInternal(this->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
JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
unsigned long flags,
std::list<std::string>& errors,
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_dictionary* sch_dict = dynamic_cast<JSON_dictionary*>(sch_v);
JSON_string* sch_str = dynamic_cast<JSON_string*>(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<std::string, PointerHolder<JSON_value> >::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<std::string, PointerHolder<JSON_value> >::iterator iter =
for (std::map<std::string, PointerHolder<JSON_value>>::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();
}

View File

@ -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<std::string> errors;
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();
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": {
"<objid>": {
"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": {
"<objid>": {
"z": "ebra"
}
}
}
)");
check_schema(good, schema, 0, false, "not optional");
check_schema(good, schema, JSON::f_optional, true, "pass");
}
int main()

View File

@ -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

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 ".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
---
end of json tests