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 363  
364 364 JSON(std::unique_ptr<JSON_value>);
365 365  
366   - static bool checkSchemaInternal(
  366 + static void checkSchemaInternal(
367 367 JSON_value* this_v,
368 368 JSON_value* sch_v,
369 369 unsigned long flags,
... ...
libqpdf/JSON.cc
... ... @@ -442,16 +442,24 @@ JSON::forEachArrayItem(std::function&lt;void(JSON value)&gt; 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
... ...