Commit 8dea480c9f065fdac76f848ed9ec7a07fd1e9870

Authored by Jay Berkenbilt
1 parent 558ba282

Allow optional fields in json "schema" checks

ChangeLog
1 1 2022-01-22 Jay Berkenbilt <ejb@ql.org>
2 2  
  3 + * JSON: for (qpdf-specific, not official) "schema" checking, add
  4 + the ability to treat missing fields as optional. Also ensure that
  5 + values in the schema are dictionary, array, or string.
  6 +
3 7 * Add convenience methods isNameAndEquals and isDictionaryOfType
4 8 to QPDFObjectHandle with corresponding functions added to the C
5 9 API. Thanks to m-holger for the contribution.
... ...
include/qpdf/JSON.hh
... ... @@ -107,21 +107,39 @@ class JSON
107 107 // single-element arrays, and strings only.
108 108 // * Recursively walk the schema
109 109 // * If the current value is a dictionary, this object must have
110   - // a dictionary in the same place with the same keys
  110 + // a dictionary in the same place with the same keys. If flags
  111 + // contains f_optional, a key in the schema does not have to
  112 + // be present in the object. Otherwise, all keys have to be
  113 + // present. Any key in the object must be present in the
  114 + // schema.
111 115 // * If the current value is an array, this object must have an
112 116 // array in the same place. The schema's array must contain a
113 117 // single element, which is used as a schema to validate each
114 118 // element of this object's corresponding array.
115   - // * Otherwise, the value is ignored.
  119 + // * Otherwise, the value must be a string whose value is a
  120 + // description of the object's corresponding value, which may
  121 + // have any type.
116 122 //
117 123 // QPDF's JSON output conforms to certain strict compatibility
118 124 // rules as discussed in the manual. The idea is that a JSON
119 125 // structure created manually in qpdf.cc doubles as both JSON help
120 126 // information and a schema for validating the JSON that qpdf
121 127 // generates. Any discrepancies are a bug in qpdf.
  128 + //
  129 + // Flags is a bitwise or of values from check_flags_e.
  130 + enum check_flags_e {
  131 + f_none = 0,
  132 + f_optional = 1 << 0,
  133 + };
  134 + QPDF_DLL
  135 + bool checkSchema(JSON schema, unsigned long flags,
  136 + std::list<std::string>& errors);
  137 +
  138 + // Same as passing 0 for flags
122 139 QPDF_DLL
123 140 bool checkSchema(JSON schema, std::list<std::string>& errors);
124 141  
  142 +
125 143 // Create a JSON object from a string.
126 144 QPDF_DLL
127 145 static JSON parse(std::string const&);
... ... @@ -180,6 +198,7 @@ class JSON
180 198  
181 199 static bool
182 200 checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
  201 + unsigned long flags,
183 202 std::list<std::string>& errors,
184 203 std::string prefix);
185 204  
... ...
libqpdf/JSON.cc
... ... @@ -394,12 +394,21 @@ JSON::checkSchema(JSON schema, std::list&lt;std::string&gt;&amp; errors)
394 394 {
395 395 return checkSchemaInternal(this->m->value.getPointer(),
396 396 schema.m->value.getPointer(),
397   - errors, "");
  397 + 0, errors, "");
398 398 }
399 399  
  400 +bool
  401 +JSON::checkSchema(JSON schema, unsigned long flags,
  402 + std::list<std::string>& errors)
  403 +{
  404 + return checkSchemaInternal(this->m->value.getPointer(),
  405 + schema.m->value.getPointer(),
  406 + flags, errors, "");
  407 +}
400 408  
401 409 bool
402 410 JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
  411 + unsigned long flags,
403 412 std::list<std::string>& errors,
404 413 std::string prefix)
405 414 {
... ... @@ -409,6 +418,8 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
409 418 JSON_array* sch_arr = dynamic_cast<JSON_array*>(sch_v);
410 419 JSON_dictionary* sch_dict = dynamic_cast<JSON_dictionary*>(sch_v);
411 420  
  421 + JSON_string* sch_str = dynamic_cast<JSON_string*>(sch_v);
  422 +
412 423 std::string err_prefix;
413 424 if (prefix.empty())
414 425 {
... ... @@ -446,34 +457,38 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
446 457 {
447 458 std::string const& key = iter.first;
448 459 checkSchemaInternal(
449   - this_dict->members[key].getPointer(),
450   - pattern_schema,
451   - errors, prefix + "." + key);
  460 + this_dict->members[key].getPointer(), pattern_schema,
  461 + flags, errors, prefix + "." + key);
452 462 }
453 463 }
454 464 else if (sch_dict)
455 465 {
456   - for (std::map<std::string, PointerHolder<JSON_value> >::iterator iter =
457   - sch_dict->members.begin();
458   - iter != sch_dict->members.end(); ++iter)
  466 + for (auto& iter: sch_dict->members)
459 467 {
460   - std::string const& key = (*iter).first;
  468 + std::string const& key = iter.first;
461 469 if (this_dict->members.count(key))
462 470 {
463 471 checkSchemaInternal(
464 472 this_dict->members[key].getPointer(),
465   - (*iter).second.getPointer(),
466   - errors, prefix + "." + key);
  473 + iter.second.getPointer(),
  474 + flags, errors, prefix + "." + key);
467 475 }
468 476 else
469 477 {
470   - QTC::TC("libtests", "JSON key missing in object");
471   - errors.push_back(
472   - err_prefix + ": key \"" + key +
473   - "\" is present in schema but missing in object");
  478 + if (flags & f_optional)
  479 + {
  480 + QTC::TC("libtests", "JSON optional key");
  481 + }
  482 + else
  483 + {
  484 + QTC::TC("libtests", "JSON key missing in object");
  485 + errors.push_back(
  486 + err_prefix + ": key \"" + key +
  487 + "\" is present in schema but missing in object");
  488 + }
474 489 }
475 490 }
476   - for (std::map<std::string, PointerHolder<JSON_value> >::iterator iter =
  491 + for (std::map<std::string, PointerHolder<JSON_value>>::iterator iter =
477 492 this_dict->members.begin();
478 493 iter != this_dict->members.end(); ++iter)
479 494 {
... ... @@ -510,9 +525,16 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
510 525 checkSchemaInternal(
511 526 (*iter).getPointer(),
512 527 sch_arr->elements.at(0).getPointer(),
513   - errors, prefix + "." + QUtil::int_to_string(i));
  528 + flags, errors, prefix + "." + QUtil::int_to_string(i));
514 529 }
515 530 }
  531 + else if (! sch_str)
  532 + {
  533 + QTC::TC("libtests", "JSON schema other type");
  534 + errors.push_back(err_prefix +
  535 + " schema value is not dictionary, array, or string");
  536 + return false;
  537 + }
516 538  
517 539 return errors.empty();
518 540 }
... ...
libtests/json.cc
... ... @@ -112,12 +112,12 @@ static void test_main()
112 112 assert(dvalue == xdvalue);
113 113 }
114 114  
115   -static void check_schema(JSON& obj, JSON& schema, bool exp,
116   - std::string const& description)
  115 +static void check_schema(JSON& obj, JSON& schema, unsigned long flags,
  116 + bool exp, std::string const& description)
117 117 {
118 118 std::list<std::string> errors;
119 119 std::cout << "--- " << description << std::endl;
120   - assert(exp == obj.checkSchema(schema, errors));
  120 + assert(exp == obj.checkSchema(schema, flags, errors));
121 121 for (std::list<std::string>::iterator iter = errors.begin();
122 122 iter != errors.end(); ++iter)
123 123 {
... ... @@ -134,8 +134,7 @@ static void test_schema()
134 134 "a": {
135 135 "q": "queue",
136 136 "r": {
137   - "x": "ecks",
138   - "y": "(bool) why"
  137 + "x": "ecks"
139 138 },
140 139 "s": [
141 140 "esses"
... ... @@ -151,14 +150,14 @@ static void test_schema()
151 150 "three": {
152 151 "<objid>": {
153 152 "z": "ebra",
154   - "o": "(optional, string) optional"
  153 + "o": "ptional"
155 154 }
156 155 }
157 156 }
158 157 )");
159 158  
160 159 JSON a = JSON::parse(R"(["not a", "dictionary"])");
161   - check_schema(a, schema, false, "top-level type mismatch");
  160 + check_schema(a, schema, 0, false, "top-level type mismatch");
162 161 JSON b = JSON::parse(R"(
163 162 {
164 163 "one": {
... ... @@ -205,10 +204,42 @@ static void test_schema()
205 204 }
206 205 )");
207 206  
208   - check_schema(b, schema, false, "missing items");
209   - check_schema(a, a, false, "top-level schema array error");
210   - check_schema(b, b, false, "lower-level schema array error");
211   - check_schema(schema, schema, true, "pass");
  207 + check_schema(b, schema, 0, false, "missing items");
  208 + check_schema(a, a, 0, false, "top-level schema array error");
  209 + check_schema(b, b, 0, false, "lower-level schema array error");
  210 +
  211 + JSON bad_schema = JSON::parse(R"({"a": true, "b": "potato?"})");
  212 + check_schema(bad_schema, bad_schema, 0, false, "bad schema field type");
  213 +
  214 + JSON good = JSON::parse(R"(
  215 +{
  216 + "one": {
  217 + "a": {
  218 + "q": "potato",
  219 + "r": {
  220 + "x": [1, null]
  221 + },
  222 + "s": [
  223 + null,
  224 + "anything"
  225 + ]
  226 + }
  227 + },
  228 + "two": [
  229 + {
  230 + "glarp": "enspliel",
  231 + "goose": 3.14
  232 + }
  233 + ],
  234 + "three": {
  235 + "<objid>": {
  236 + "z": "ebra"
  237 + }
  238 + }
  239 +}
  240 +)");
  241 + check_schema(good, schema, 0, false, "not optional");
  242 + check_schema(good, schema, JSON::f_optional, true, "pass");
212 243 }
213 244  
214 245 int main()
... ...
libtests/libtests.testcov
... ... @@ -89,3 +89,5 @@ JSON parse premature end of u 0
89 89 JSON parse bad hex after u 0
90 90 JSONHandler unhandled value 0
91 91 JSONHandler unexpected key 0
  92 +JSON schema other type 0
  93 +JSON optional key 0
... ...
libtests/qtest/json/json.out
... ... @@ -21,6 +21,12 @@ top-level object schema array contains other than one item
21 21 json key ".one.a.r" schema array contains other than one item
22 22 json key ".two" schema array contains other than one item
23 23 ---
  24 +--- bad schema field type
  25 +json key ".a" schema value is not dictionary, array, or string
  26 +---
  27 +--- not optional
  28 +json key ".three.<objid>": key "o" is present in schema but missing in object
  29 +---
24 30 --- pass
25 31 ---
26 32 end of json tests
... ...