Commit af2bdc80b35a3ee901b27806bef64c16bfdcf234
Committed by
GitHub
Merge pull request #1603 from m-holger/schema
Improve JSON schema validation and error reporting
Showing
3 changed files
with
48 additions
and
62 deletions
include/qpdf/JSON.hh
libqpdf/JSON.cc
| ... | ... | @@ -442,16 +442,24 @@ JSON::forEachArrayItem(std::function<void(JSON value)> fn) const |
| 442 | 442 | bool |
| 443 | 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 | 452 | bool |
| 449 | 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 | 463 | JSON::checkSchemaInternal( |
| 456 | 464 | JSON_value* this_v, |
| 457 | 465 | JSON_value* sch_v, |
| ... | ... | @@ -459,44 +467,33 @@ JSON::checkSchemaInternal( |
| 459 | 467 | std::list<std::string>& errors, |
| 460 | 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 | 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 | 497 | for (auto& [key, val]: sch_dict->members) { |
| 501 | 498 | if (this_dict->members.contains(key)) { |
| 502 | 499 | checkSchemaInternal( |
| ... | ... | @@ -509,22 +506,21 @@ JSON::checkSchemaInternal( |
| 509 | 506 | if (flags & f_optional) { |
| 510 | 507 | QTC::TC("libtests", "JSON optional key"); |
| 511 | 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 | 513 | for (auto const& item: this_dict->members) { |
| 520 | 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 | 524 | auto n_elements = sch_arr->elements.size(); |
| 529 | 525 | if (n_elements == 1) { |
| 530 | 526 | // A single-element array in the schema allows a single element in the object or a |
| ... | ... | @@ -543,15 +539,12 @@ JSON::checkSchemaInternal( |
| 543 | 539 | ++i; |
| 544 | 540 | } |
| 545 | 541 | } else { |
| 546 | - QTC::TC("libtests", "JSON schema array for single item"); | |
| 547 | 542 | checkSchemaInternal( |
| 548 | 543 | this_v, sch_arr->elements.at(0).m->value.get(), flags, errors, prefix); |
| 549 | 544 | } |
| 550 | 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 | 548 | } else { |
| 556 | 549 | // A multi-element array in the schema must correspond to an element of the same length |
| 557 | 550 | // in the object. Each element in the object is validated against the corresponding |
| ... | ... | @@ -567,13 +560,12 @@ JSON::checkSchemaInternal( |
| 567 | 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 | 571 | namespace | ... | ... |
libtests/libtests.testcov
| ... | ... | @@ -31,9 +31,6 @@ Pl_PNGFilter decodeUp 0 |
| 31 | 31 | Pl_PNGFilter decodeAverage 0 |
| 32 | 32 | Pl_PNGFilter decodePaeth 0 |
| 33 | 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 | 34 | QPDFArgParser read args from stdin 0 |
| 38 | 35 | QPDFArgParser read args from file 0 |
| 39 | 36 | QPDFArgParser required choices 0 |
| ... | ... | @@ -72,10 +69,7 @@ JSON parse ls premature end of input 0 |
| 72 | 69 | JSON parse bad hex after u 0 |
| 73 | 70 | JSONHandler unhandled value 0 |
| 74 | 71 | JSONHandler unexpected key 0 |
| 75 | -JSON schema other type 0 | |
| 76 | 72 | JSON optional key 0 |
| 77 | 73 | JSON 16 high high 0 |
| 78 | 74 | JSON 16 low not after high 0 |
| 79 | 75 | JSON 16 dangling high 0 |
| 80 | -JSON schema array for single item 0 | |
| 81 | -JSON schema array length mismatch 0 | ... | ... |