Commit 8dea480c9f065fdac76f848ed9ec7a07fd1e9870

Authored by Jay Berkenbilt
1 parent 558ba282

Allow optional fields in json "schema" checks

ChangeLog
1 2022-01-22 Jay Berkenbilt <ejb@ql.org> 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 * Add convenience methods isNameAndEquals and isDictionaryOfType 7 * Add convenience methods isNameAndEquals and isDictionaryOfType
4 to QPDFObjectHandle with corresponding functions added to the C 8 to QPDFObjectHandle with corresponding functions added to the C
5 API. Thanks to m-holger for the contribution. 9 API. Thanks to m-holger for the contribution.
include/qpdf/JSON.hh
@@ -107,21 +107,39 @@ class JSON @@ -107,21 +107,39 @@ class JSON
107 // single-element arrays, and strings only. 107 // single-element arrays, and strings only.
108 // * Recursively walk the schema 108 // * Recursively walk the schema
109 // * If the current value is a dictionary, this object must have 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 // * If the current value is an array, this object must have an 115 // * If the current value is an array, this object must have an
112 // array in the same place. The schema's array must contain a 116 // array in the same place. The schema's array must contain a
113 // single element, which is used as a schema to validate each 117 // single element, which is used as a schema to validate each
114 // element of this object's corresponding array. 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 // QPDF's JSON output conforms to certain strict compatibility 123 // QPDF's JSON output conforms to certain strict compatibility
118 // rules as discussed in the manual. The idea is that a JSON 124 // rules as discussed in the manual. The idea is that a JSON
119 // structure created manually in qpdf.cc doubles as both JSON help 125 // structure created manually in qpdf.cc doubles as both JSON help
120 // information and a schema for validating the JSON that qpdf 126 // information and a schema for validating the JSON that qpdf
121 // generates. Any discrepancies are a bug in qpdf. 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 QPDF_DLL 139 QPDF_DLL
123 bool checkSchema(JSON schema, std::list<std::string>& errors); 140 bool checkSchema(JSON schema, std::list<std::string>& errors);
124 141
  142 +
125 // Create a JSON object from a string. 143 // Create a JSON object from a string.
126 QPDF_DLL 144 QPDF_DLL
127 static JSON parse(std::string const&); 145 static JSON parse(std::string const&);
@@ -180,6 +198,7 @@ class JSON @@ -180,6 +198,7 @@ class JSON
180 198
181 static bool 199 static bool
182 checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, 200 checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
  201 + unsigned long flags,
183 std::list<std::string>& errors, 202 std::list<std::string>& errors,
184 std::string prefix); 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,12 +394,21 @@ JSON::checkSchema(JSON schema, std::list&lt;std::string&gt;&amp; errors)
394 { 394 {
395 return checkSchemaInternal(this->m->value.getPointer(), 395 return checkSchemaInternal(this->m->value.getPointer(),
396 schema.m->value.getPointer(), 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 bool 409 bool
402 JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, 410 JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
  411 + unsigned long flags,
403 std::list<std::string>& errors, 412 std::list<std::string>& errors,
404 std::string prefix) 413 std::string prefix)
405 { 414 {
@@ -409,6 +418,8 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, @@ -409,6 +418,8 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
409 JSON_array* sch_arr = dynamic_cast<JSON_array*>(sch_v); 418 JSON_array* sch_arr = dynamic_cast<JSON_array*>(sch_v);
410 JSON_dictionary* sch_dict = dynamic_cast<JSON_dictionary*>(sch_v); 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 std::string err_prefix; 423 std::string err_prefix;
413 if (prefix.empty()) 424 if (prefix.empty())
414 { 425 {
@@ -446,34 +457,38 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, @@ -446,34 +457,38 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
446 { 457 {
447 std::string const& key = iter.first; 458 std::string const& key = iter.first;
448 checkSchemaInternal( 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 else if (sch_dict) 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 if (this_dict->members.count(key)) 469 if (this_dict->members.count(key))
462 { 470 {
463 checkSchemaInternal( 471 checkSchemaInternal(
464 this_dict->members[key].getPointer(), 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 else 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 this_dict->members.begin(); 492 this_dict->members.begin();
478 iter != this_dict->members.end(); ++iter) 493 iter != this_dict->members.end(); ++iter)
479 { 494 {
@@ -510,9 +525,16 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v, @@ -510,9 +525,16 @@ JSON::checkSchemaInternal(JSON_value* this_v, JSON_value* sch_v,
510 checkSchemaInternal( 525 checkSchemaInternal(
511 (*iter).getPointer(), 526 (*iter).getPointer(),
512 sch_arr->elements.at(0).getPointer(), 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 return errors.empty(); 539 return errors.empty();
518 } 540 }
libtests/json.cc
@@ -112,12 +112,12 @@ static void test_main() @@ -112,12 +112,12 @@ static void test_main()
112 assert(dvalue == xdvalue); 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 std::list<std::string> errors; 118 std::list<std::string> errors;
119 std::cout << "--- " << description << std::endl; 119 std::cout << "--- " << description << std::endl;
120 - assert(exp == obj.checkSchema(schema, errors)); 120 + assert(exp == obj.checkSchema(schema, flags, errors));
121 for (std::list<std::string>::iterator iter = errors.begin(); 121 for (std::list<std::string>::iterator iter = errors.begin();
122 iter != errors.end(); ++iter) 122 iter != errors.end(); ++iter)
123 { 123 {
@@ -134,8 +134,7 @@ static void test_schema() @@ -134,8 +134,7 @@ static void test_schema()
134 "a": { 134 "a": {
135 "q": "queue", 135 "q": "queue",
136 "r": { 136 "r": {
137 - "x": "ecks",  
138 - "y": "(bool) why" 137 + "x": "ecks"
139 }, 138 },
140 "s": [ 139 "s": [
141 "esses" 140 "esses"
@@ -151,14 +150,14 @@ static void test_schema() @@ -151,14 +150,14 @@ static void test_schema()
151 "three": { 150 "three": {
152 "<objid>": { 151 "<objid>": {
153 "z": "ebra", 152 "z": "ebra",
154 - "o": "(optional, string) optional" 153 + "o": "ptional"
155 } 154 }
156 } 155 }
157 } 156 }
158 )"); 157 )");
159 158
160 JSON a = JSON::parse(R"(["not a", "dictionary"])"); 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 JSON b = JSON::parse(R"( 161 JSON b = JSON::parse(R"(
163 { 162 {
164 "one": { 163 "one": {
@@ -205,10 +204,42 @@ static void test_schema() @@ -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 int main() 245 int main()
libtests/libtests.testcov
@@ -89,3 +89,5 @@ JSON parse premature end of u 0 @@ -89,3 +89,5 @@ JSON parse premature end of u 0
89 JSON parse bad hex after u 0 89 JSON parse bad hex after u 0
90 JSONHandler unhandled value 0 90 JSONHandler unhandled value 0
91 JSONHandler unexpected key 0 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,6 +21,12 @@ top-level object schema array contains other than one item
21 json key ".one.a.r" schema array contains other than one item 21 json key ".one.a.r" schema array contains other than one item
22 json key ".two" schema array contains other than one item 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 --- pass 30 --- pass
25 --- 31 ---
26 end of json tests 32 end of json tests