Commit 4b065d205870f45f40c5ae18cb014a9861a63d3e
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.
Showing
3 changed files
with
48 additions
and
62 deletions
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<void(JSON value)> fn) const | @@ -442,16 +442,24 @@ JSON::forEachArrayItem(std::function<void(JSON value)> 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 |