Commit 4b065d205870f45f40c5ae18cb014a9861a63d3e

Authored by m-holger
1 parent 52e41f25

Improve JSON schema validation and error reporting

- Add null pointer checks for schema objects.
- Refactor `checkSchemaInternal` for better readability and maintainability.
- Replace redundant error construction logic with lambda-based approach.
- Simplify array and dictionary schema handling.
- Remove unused test coverage statistics.
include/qpdf/JSON.hh
@@ -363,7 +363,7 @@ class JSON @@ -363,7 +363,7 @@ class JSON
363 363
364 JSON(std::unique_ptr<JSON_value>); 364 JSON(std::unique_ptr<JSON_value>);
365 365
366 - static bool checkSchemaInternal( 366 + static void checkSchemaInternal(
367 JSON_value* this_v, 367 JSON_value* this_v,
368 JSON_value* sch_v, 368 JSON_value* sch_v,
369 unsigned long flags, 369 unsigned long flags,
libqpdf/JSON.cc
@@ -442,16 +442,24 @@ JSON::forEachArrayItem(std::function&lt;void(JSON value)&gt; fn) const @@ -442,16 +442,24 @@ JSON::forEachArrayItem(std::function&lt;void(JSON value)&gt; fn) const
442 bool 442 bool
443 JSON::checkSchema(JSON schema, std::list<std::string>& errors) 443 JSON::checkSchema(JSON schema, std::list<std::string>& errors)
444 { 444 {
445 - return m && checkSchemaInternal(m->value.get(), schema.m->value.get(), 0, errors, ""); 445 + if (!m || !schema.m) {
  446 + return false;
  447 + }
  448 + checkSchemaInternal(m->value.get(), schema.m->value.get(), 0, errors, "");
  449 + return errors.empty();
446 } 450 }
447 451
448 bool 452 bool
449 JSON::checkSchema(JSON schema, unsigned long flags, std::list<std::string>& errors) 453 JSON::checkSchema(JSON schema, unsigned long flags, std::list<std::string>& errors)
450 { 454 {
451 - return m && checkSchemaInternal(m->value.get(), schema.m->value.get(), flags, errors, ""); 455 + if (!m || !schema.m) {
  456 + return false;
  457 + }
  458 + checkSchemaInternal(m->value.get(), schema.m->value.get(), flags, errors, "");
  459 + return errors.empty();
452 } 460 }
453 461
454 -bool 462 +void
455 JSON::checkSchemaInternal( 463 JSON::checkSchemaInternal(
456 JSON_value* this_v, 464 JSON_value* this_v,
457 JSON_value* sch_v, 465 JSON_value* sch_v,
@@ -459,44 +467,33 @@ JSON::checkSchemaInternal( @@ -459,44 +467,33 @@ JSON::checkSchemaInternal(
459 std::list<std::string>& errors, 467 std::list<std::string>& errors,
460 std::string prefix) 468 std::string prefix)
461 { 469 {
462 - auto* this_arr = dynamic_cast<JSON_array*>(this_v);  
463 - auto* this_dict = dynamic_cast<JSON_dictionary*>(this_v);  
464 -  
465 - auto* sch_arr = dynamic_cast<JSON_array*>(sch_v);  
466 - auto* sch_dict = dynamic_cast<JSON_dictionary*>(sch_v);  
467 -  
468 - auto* sch_str = dynamic_cast<JSON_string*>(sch_v);  
469 -  
470 - std::string err_prefix;  
471 - if (prefix.empty()) {  
472 - err_prefix = "top-level object";  
473 - } else {  
474 - err_prefix = "json key \"" + prefix + "\"";  
475 - } 470 + auto error = [&errors, prefix](std::string const& msg) {
  471 + if (prefix.empty()) {
  472 + errors.emplace_back("top-level object" + msg);
  473 + } else {
  474 + errors.emplace_back("json key \"" + prefix + "\"" + msg);
  475 + }
  476 + };
476 477
477 - std::string pattern_key;  
478 - if (sch_dict) { 478 + if (auto* sch_dict = dynamic_cast<JSON_dictionary*>(sch_v)) {
  479 + auto* this_dict = dynamic_cast<JSON_dictionary*>(this_v);
479 if (!this_dict) { 480 if (!this_dict) {
480 - QTC::TC("libtests", "JSON wanted dictionary");  
481 - errors.push_back(err_prefix + " is supposed to be a dictionary");  
482 - return false; 481 + error(" is supposed to be a dictionary");
  482 + return;
483 } 483 }
484 - auto members = sch_dict->members;  
485 - std::string key;  
486 - if ((members.size() == 1) &&  
487 - ((key = members.begin()->first, key.length() > 2) && (key.at(0) == '<') &&  
488 - (key.at(key.length() - 1) == '>'))) {  
489 - pattern_key = key; 484 + auto const& members = sch_dict->members;
  485 + if (members.size() == 1) {
  486 + auto const& pattern_key = members.begin()->first;
  487 + if (pattern_key.starts_with('<') && pattern_key.ends_with('>')) {
  488 + auto pattern_schema = sch_dict->members[pattern_key].m->value.get();
  489 + for (auto const& [key, val]: this_dict->members) {
  490 + checkSchemaInternal(
  491 + val.m->value.get(), pattern_schema, flags, errors, prefix + "." + key);
  492 + }
  493 + return;
  494 + }
490 } 495 }
491 - }  
492 496
493 - if (sch_dict && !pattern_key.empty()) {  
494 - auto pattern_schema = sch_dict->members[pattern_key].m->value.get();  
495 - for (auto const& [key, val]: this_dict->members) {  
496 - checkSchemaInternal(  
497 - val.m->value.get(), pattern_schema, flags, errors, prefix + "." + key);  
498 - }  
499 - } else if (sch_dict) {  
500 for (auto& [key, val]: sch_dict->members) { 497 for (auto& [key, val]: sch_dict->members) {
501 if (this_dict->members.contains(key)) { 498 if (this_dict->members.contains(key)) {
502 checkSchemaInternal( 499 checkSchemaInternal(
@@ -509,22 +506,21 @@ JSON::checkSchemaInternal( @@ -509,22 +506,21 @@ JSON::checkSchemaInternal(
509 if (flags & f_optional) { 506 if (flags & f_optional) {
510 QTC::TC("libtests", "JSON optional key"); 507 QTC::TC("libtests", "JSON optional key");
511 } else { 508 } else {
512 - QTC::TC("libtests", "JSON key missing in object");  
513 - errors.emplace_back(  
514 - err_prefix + ": key \"" + key +  
515 - "\" is present in schema but missing in object"); 509 + error(": key \"" + key + "\" is present in schema but missing in object");
516 } 510 }
517 } 511 }
518 } 512 }
519 for (auto const& item: this_dict->members) { 513 for (auto const& item: this_dict->members) {
520 if (!sch_dict->members.contains(item.first)) { 514 if (!sch_dict->members.contains(item.first)) {
521 - QTC::TC("libtests", "JSON key extra in object");  
522 - errors.emplace_back(  
523 - err_prefix + ": key \"" + item.first +  
524 - "\" is not present in schema but appears in object"); 515 + error(
  516 + ": key \"" + item.first + "\" is not present in schema but appears in object");
525 } 517 }
526 } 518 }
527 - } else if (sch_arr) { 519 + return;
  520 + }
  521 +
  522 + if (auto* sch_arr = dynamic_cast<JSON_array*>(sch_v)) {
  523 + auto* this_arr = dynamic_cast<JSON_array*>(this_v);
528 auto n_elements = sch_arr->elements.size(); 524 auto n_elements = sch_arr->elements.size();
529 if (n_elements == 1) { 525 if (n_elements == 1) {
530 // A single-element array in the schema allows a single element in the object or a 526 // A single-element array in the schema allows a single element in the object or a
@@ -543,15 +539,12 @@ JSON::checkSchemaInternal( @@ -543,15 +539,12 @@ JSON::checkSchemaInternal(
543 ++i; 539 ++i;
544 } 540 }
545 } else { 541 } else {
546 - QTC::TC("libtests", "JSON schema array for single item");  
547 checkSchemaInternal( 542 checkSchemaInternal(
548 this_v, sch_arr->elements.at(0).m->value.get(), flags, errors, prefix); 543 this_v, sch_arr->elements.at(0).m->value.get(), flags, errors, prefix);
549 } 544 }
550 } else if (!this_arr || this_arr->elements.size() != n_elements) { 545 } else if (!this_arr || this_arr->elements.size() != n_elements) {
551 - QTC::TC("libtests", "JSON schema array length mismatch");  
552 - errors.emplace_back(  
553 - err_prefix + " is supposed to be an array of length " + std::to_string(n_elements));  
554 - return false; 546 + error(" is supposed to be an array of length " + std::to_string(n_elements));
  547 + return;
555 } else { 548 } else {
556 // A multi-element array in the schema must correspond to an element of the same length 549 // A multi-element array in the schema must correspond to an element of the same length
557 // in the object. Each element in the object is validated against the corresponding 550 // in the object. Each element in the object is validated against the corresponding
@@ -567,13 +560,12 @@ JSON::checkSchemaInternal( @@ -567,13 +560,12 @@ JSON::checkSchemaInternal(
567 ++i; 560 ++i;
568 } 561 }
569 } 562 }
570 - } else if (!sch_str) {  
571 - QTC::TC("libtests", "JSON schema other type");  
572 - errors.emplace_back(err_prefix + " schema value is not dictionary, array, or string");  
573 - return false; 563 + return;
574 } 564 }
575 565
576 - return errors.empty(); 566 + if (!dynamic_cast<JSON_string*>(sch_v)) {
  567 + error(" schema value is not dictionary, array, or string");
  568 + }
577 } 569 }
578 570
579 namespace 571 namespace
libtests/libtests.testcov
@@ -31,9 +31,6 @@ Pl_PNGFilter decodeUp 0 @@ -31,9 +31,6 @@ Pl_PNGFilter decodeUp 0
31 Pl_PNGFilter decodeAverage 0 31 Pl_PNGFilter decodeAverage 0
32 Pl_PNGFilter decodePaeth 0 32 Pl_PNGFilter decodePaeth 0
33 Pl_TIFFPredictor processRow 1 33 Pl_TIFFPredictor processRow 1
34 -JSON wanted dictionary 0  
35 -JSON key missing in object 0  
36 -JSON key extra in object 0  
37 QPDFArgParser read args from stdin 0 34 QPDFArgParser read args from stdin 0
38 QPDFArgParser read args from file 0 35 QPDFArgParser read args from file 0
39 QPDFArgParser required choices 0 36 QPDFArgParser required choices 0
@@ -72,10 +69,7 @@ JSON parse ls premature end of input 0 @@ -72,10 +69,7 @@ JSON parse ls premature end of input 0
72 JSON parse bad hex after u 0 69 JSON parse bad hex after u 0
73 JSONHandler unhandled value 0 70 JSONHandler unhandled value 0
74 JSONHandler unexpected key 0 71 JSONHandler unexpected key 0
75 -JSON schema other type 0  
76 JSON optional key 0 72 JSON optional key 0
77 JSON 16 high high 0 73 JSON 16 high high 0
78 JSON 16 low not after high 0 74 JSON 16 low not after high 0
79 JSON 16 dangling high 0 75 JSON 16 dangling high 0
80 -JSON schema array for single item 0  
81 -JSON schema array length mismatch 0