Commit f8d1ab946205440ed3c44511ef42e5ad13fb9e5e
1 parent
b3e6d445
JSON schema -- accept single item in place of array
When the schema wants a variable-length array, allow a single item as well as allowing an array.
Showing
7 changed files
with
46 additions
and
28 deletions
ChangeLog
| 1 | 1 | 2022-07-24 Jay Berkenbilt <ejb@ql.org> |
| 2 | 2 | |
| 3 | + * include/qpdf/JSON.hh: Schema validation: allow a single item to | |
| 4 | + appear anywhere that the schema has an array of a single item. | |
| 5 | + This makes it possible to change an element of the schema from an | |
| 6 | + item to an array to allow the data to accept an array where a | |
| 7 | + single value was previously required. This change is needed to | |
| 8 | + allow QPDFJob JSON to start accepting multiple items where a | |
| 9 | + single item used to be expected without breaking backward | |
| 10 | + compatibility. Without this change, the earlier fix to | |
| 11 | + removeAttachment would be a breaking change. | |
| 12 | + | |
| 3 | 13 | * QPDFObjectHandle: for the methods insertItem, appendItem, |
| 4 | 14 | eraseItem, replaceKey, and removeKey, add a corresponding |
| 5 | 15 | "AndGetNew" and/or "AndGetOld" methods. The ones that end with | ... | ... |
TODO
| ... | ... | @@ -19,10 +19,6 @@ Pending changes: |
| 19 | 19 | appimage build specifically is setting the runpath, which is |
| 20 | 20 | actually desirable in this case. Make sure to understand and |
| 21 | 21 | document this. Maybe add a check for it in the build. |
| 22 | -* Make job JSON accept a single element and treat as an array of one | |
| 23 | - when an array is expected. This allows for making things repeatable | |
| 24 | - in the future without breaking compatibility and is needed for the | |
| 25 | - remote-attachment fix to be backward-compatible. | |
| 26 | 22 | * Decide what to do about #664 (get*Box) |
| 27 | 23 | * Add an option --ignore-encryption to ignore encryption information |
| 28 | 24 | and treat encrypted files as if they weren't encrypted. This should | ... | ... |
include/qpdf/JSON.hh
| ... | ... | @@ -184,6 +184,14 @@ class JSON |
| 184 | 184 | // * The schema is a nested structure containing dictionaries, |
| 185 | 185 | // single-element arrays, and strings only. |
| 186 | 186 | // * Recursively walk the schema. |
| 187 | + // * Whenever the schema has an array of length 1 and the object | |
| 188 | + // does not have an array in the corresponding location, | |
| 189 | + // validate the object against the array's single element. | |
| 190 | + // This effectively enables a single element to appear in | |
| 191 | + // place of an array and be treated as if it were an array of | |
| 192 | + // one element. This makes it possible to decide later that | |
| 193 | + // something that used to contain a single element now allows | |
| 194 | + // an array without invalidating any old data. | |
| 187 | 195 | // * If the current value is a dictionary, this object must have |
| 188 | 196 | // a dictionary in the same place with the same keys. If flags |
| 189 | 197 | // contains f_optional, a key in the schema does not have to | ... | ... |
libqpdf/JSON.cc
| ... | ... | @@ -538,26 +538,27 @@ JSON::checkSchemaInternal( |
| 538 | 538 | } |
| 539 | 539 | } |
| 540 | 540 | } else if (sch_arr) { |
| 541 | - if (!this_arr) { | |
| 542 | - QTC::TC("libtests", "JSON wanted array"); | |
| 543 | - errors.push_back(err_prefix + " is supposed to be an array"); | |
| 544 | - return false; | |
| 545 | - } | |
| 546 | 541 | if (sch_arr->elements.size() != 1) { |
| 547 | 542 | QTC::TC("libtests", "JSON schema array error"); |
| 548 | 543 | errors.push_back( |
| 549 | 544 | err_prefix + " schema array contains other than one item"); |
| 550 | 545 | return false; |
| 551 | 546 | } |
| 552 | - int i = 0; | |
| 553 | - for (auto const& element: this_arr->elements) { | |
| 547 | + if (this_arr) { | |
| 548 | + int i = 0; | |
| 549 | + for (auto const& element: this_arr->elements) { | |
| 550 | + checkSchemaInternal( | |
| 551 | + element.get(), | |
| 552 | + sch_arr->elements.at(0).get(), | |
| 553 | + flags, | |
| 554 | + errors, | |
| 555 | + prefix + "." + QUtil::int_to_string(i)); | |
| 556 | + ++i; | |
| 557 | + } | |
| 558 | + } else { | |
| 559 | + QTC::TC("libtests", "JSON schema array for single item"); | |
| 554 | 560 | checkSchemaInternal( |
| 555 | - element.get(), | |
| 556 | - sch_arr->elements.at(0).get(), | |
| 557 | - flags, | |
| 558 | - errors, | |
| 559 | - prefix + "." + QUtil::int_to_string(i)); | |
| 560 | - ++i; | |
| 561 | + this_v, sch_arr->elements.at(0).get(), flags, errors, prefix); | |
| 561 | 562 | } |
| 562 | 563 | } else if (!sch_str) { |
| 563 | 564 | QTC::TC("libtests", "JSON schema other type"); | ... | ... |
libtests/json.cc
| ... | ... | @@ -162,7 +162,9 @@ test_schema() |
| 162 | 162 | "x": "ecks" |
| 163 | 163 | }, |
| 164 | 164 | "s": [ |
| 165 | - "esses" | |
| 165 | + { | |
| 166 | + "ss": "esses" | |
| 167 | + } | |
| 166 | 168 | ] |
| 167 | 169 | } |
| 168 | 170 | }, |
| ... | ... | @@ -236,6 +238,8 @@ test_schema() |
| 236 | 238 | JSON bad_schema = JSON::parse(R"({"a": true, "b": "potato?"})"); |
| 237 | 239 | check_schema(bad_schema, bad_schema, 0, false, "bad schema field type"); |
| 238 | 240 | |
| 241 | + // "two" exercises the case of the JSON containing a single | |
| 242 | + // element where the schema has an array. | |
| 239 | 243 | JSON good = JSON::parse(R"( |
| 240 | 244 | { |
| 241 | 245 | "one": { |
| ... | ... | @@ -245,17 +249,15 @@ test_schema() |
| 245 | 249 | "x": [1, null] |
| 246 | 250 | }, |
| 247 | 251 | "s": [ |
| 248 | - null, | |
| 249 | - "anything" | |
| 252 | + {"ss": null}, | |
| 253 | + {"ss": "anything"} | |
| 250 | 254 | ] |
| 251 | 255 | } |
| 252 | 256 | }, |
| 253 | - "two": [ | |
| 254 | - { | |
| 255 | - "glarp": "enspliel", | |
| 256 | - "goose": 3.14 | |
| 257 | - } | |
| 258 | - ], | |
| 257 | + "two": { | |
| 258 | + "glarp": "enspliel", | |
| 259 | + "goose": 3.14 | |
| 260 | + }, | |
| 259 | 261 | "three": { |
| 260 | 262 | "<objid>": { |
| 261 | 263 | "z": "ebra" | ... | ... |
libtests/libtests.testcov
| ... | ... | @@ -36,7 +36,6 @@ Pl_PNGFilter decodePaeth 0 |
| 36 | 36 | Pl_TIFFPredictor processRow 1 |
| 37 | 37 | JSON wanted dictionary 0 |
| 38 | 38 | JSON key missing in object 0 |
| 39 | -JSON wanted array 0 | |
| 40 | 39 | JSON schema array error 0 |
| 41 | 40 | JSON key extra in object 0 |
| 42 | 41 | QPDFArgParser read args from stdin 0 |
| ... | ... | @@ -93,3 +92,4 @@ JSON 16 high high 0 |
| 93 | 92 | JSON 16 low not after high 0 |
| 94 | 93 | JSON 16 dangling high 0 |
| 95 | 94 | JSON parse duplicate key 0 |
| 95 | +JSON schema array for single item 0 | ... | ... |
libtests/qtest/json/json.out
| ... | ... | @@ -4,7 +4,8 @@ top-level object is supposed to be a dictionary |
| 4 | 4 | --- missing items |
| 5 | 5 | json key ".one.a": key "q" is present in schema but missing in object |
| 6 | 6 | json key ".one.a.r" is supposed to be a dictionary |
| 7 | -json key ".one.a.s" is supposed to be an array | |
| 7 | +json key ".one.a.s": key "ss" is present in schema but missing in object | |
| 8 | +json key ".one.a.s": key "z" is not present in schema but appears in object | |
| 8 | 9 | json key ".one.a": key "t" is not present in schema but appears in object |
| 9 | 10 | json key ".three.anything": key "z" is present in schema but missing in object |
| 10 | 11 | json key ".three.anything": key "x" is not present in schema but appears in object | ... | ... |