From a9f8c5634e347f3b06a24a66d42944de406198d9 Mon Sep 17 00:00:00 2001 From: EloB <476567+EloB@users.noreply.github.com> Date: Sun, 15 Nov 2026 07:52:21 +0100 Subject: [PATCH 1/2] test: cover top-level maps of integers and top-level arrays of null `top-level-integer-map.schema` round-trips a map of integers, and `top-level-null-array.schema` round-trips `[null, null]`. Neither has an expected failure: several languages model `null` as an untyped value, and several read a top-level map without checking its values' types. PHP skips the array of null with the other top-level array, and Objective-C skips it because the generated item check (`id.class`) does not compile. Co-Authored-By: Claude Opus 5.5 --- test/inputs/schema/top-level-integer-map.1.json | 1 + test/inputs/schema/top-level-integer-map.schema | 6 ++++++ test/inputs/schema/top-level-null-array.1.json | 1 + test/inputs/schema/top-level-null-array.schema | 6 ++++++ test/languages.ts | 8 +++++++- 5 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 test/inputs/schema/top-level-integer-map.1.json create mode 100644 test/inputs/schema/top-level-integer-map.schema create mode 100644 test/inputs/schema/top-level-null-array.1.json create mode 100644 test/inputs/schema/top-level-null-array.schema diff --git a/test/inputs/schema/top-level-integer-map.1.json b/test/inputs/schema/top-level-integer-map.1.json new file mode 100644 index 000000000..c9f63a6c8 --- /dev/null +++ b/test/inputs/schema/top-level-integer-map.1.json @@ -0,0 +1 @@ +{ "a": 1, "b": -2 } diff --git a/test/inputs/schema/top-level-integer-map.schema b/test/inputs/schema/top-level-integer-map.schema new file mode 100644 index 000000000..b56b87499 --- /dev/null +++ b/test/inputs/schema/top-level-integer-map.schema @@ -0,0 +1,6 @@ +{ + "type": "object", + "additionalProperties": { + "type": "integer" + } +} diff --git a/test/inputs/schema/top-level-null-array.1.json b/test/inputs/schema/top-level-null-array.1.json new file mode 100644 index 000000000..26d218ea6 --- /dev/null +++ b/test/inputs/schema/top-level-null-array.1.json @@ -0,0 +1 @@ +[null, null] diff --git a/test/inputs/schema/top-level-null-array.schema b/test/inputs/schema/top-level-null-array.schema new file mode 100644 index 000000000..e964719f4 --- /dev/null +++ b/test/inputs/schema/top-level-null-array.schema @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "type": "null" + } +} diff --git a/test/languages.ts b/test/languages.ts index 8f534c31d..c1047e820 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -952,7 +952,12 @@ export const ObjectiveCLanguage: Language = { "blns-object.json", ], skipMiscJSON: false, - skipSchema: ["optional-property.schema"], + skipSchema: [ + "optional-property.schema", + // A top-level array of null checks its items with `id.class`, which + // does not compile. + "top-level-null-array.schema", + ], rendererOptions: { functions: "true" }, quickTestRendererOptions: [], sourceFiles: ["src/language/Objective-C/index.ts"], @@ -1722,6 +1727,7 @@ export const PHPLanguage: Language = { "recursive-union-flattening.schema", // The driver does not support top-level arrays. "issue2680-top-level-array.schema", + "top-level-null-array.schema", ], rendererOptions: {}, quickTestRendererOptions: [], From 5902704f8b0e2f8395bc561d35da9582460bf518 Mon Sep 17 00:00:00 2001 From: EloB <476567+EloB@users.noreply.github.com> Date: Sun, 15 Nov 2026 07:52:21 +0100 Subject: [PATCH 2/2] fix(cjson): fix crashes and undefined behavior in generated code - A top-level map of integers, booleans, doubles or enums stored the value itself as the pointer (which does not compile) and did not check the values' types. Allocate each value as the top-level list does, check the types as the list does, and fail the parse when an element of a top-level list or map cannot be allocated. - Top-level lists of `null` marked elements with `0xDEADBEAF` (elsewhere `0xDEADBEEF`), and the delete functions of top-level lists and maps of `null` freed the marker. Use `0xDEADBEEF`, and delete each element as a class's list or map does. - Enum getters passed `cJSON_GetStringValue(j)`, `NULL` for a value that is not a string, to `strcmp`. Check `cJSON_IsString(j)` instead of `NULL != j`; such a value reads as 0, as an unknown string does. - Pattern checks ignored what `regcomp` returns, so `regexec` and `regfree` ran on a regex that did not compile. Check only patterns that compile; a JSON Schema pattern that is not a POSIX extended regular expression is not checked. - Maps of classes and unions parsed every value twice, deleting the validated value. Store it instead. Co-Authored-By: Claude Opus 5.5 --- .../src/language/CJSON/CJSONRenderer.ts | 376 +++++++----------- 1 file changed, 143 insertions(+), 233 deletions(-) diff --git a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts index ccca25aa5..a69d1f710 100644 --- a/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts +++ b/packages/quicktype-core/src/language/CJSON/CJSONRenderer.ts @@ -536,7 +536,7 @@ export class CJSONRenderer extends ConvenienceRenderer { ], () => { this.emitLine("enum ", enumName, " x = 0;"); - this.emitBlock(["if (NULL != j)"], () => { + this.emitBlock(["if (cJSON_IsString(j))"], () => { let onFirst = true; const combinedName = allUpperWordStyle( this.sourcelikeToString(enumName), @@ -2502,7 +2502,7 @@ export class CJSONRenderer extends ConvenienceRenderer { ); if (pattern !== undefined) { this.emitLine( - `regex_t regex; regcomp(®ex, "${stringEscape(pattern)}", REG_EXTENDED); if (regexec(®ex, ${value}->valuestring, 0, NULL, 0)) { regfree(®ex); cJSON_Delete${this.sourcelikeToString(className)}(x); return NULL; } regfree(®ex);`, + `regex_t regex; if (0 == regcomp(®ex, "${stringEscape(pattern)}", REG_EXTENDED)) { if (regexec(®ex, ${value}->valuestring, 0, NULL, 0)) { regfree(®ex); cJSON_Delete${this.sourcelikeToString(className)}(x); return NULL; } regfree(®ex); }`, ); } if (minLength !== undefined) @@ -3004,21 +3004,14 @@ if (NULL == validated) { x${level > 0 ? level.toString() : ""}->${this.sourcelikeToString(name)} = x${child_level}; cJSON_Delete${this.sourcelikeToString(className)}(x); return NULL; -} -${this.sourcelikeToString(cJSON.items?.deleteType ?? "")}(validated);`, +}`, ); this.emitLine( "hashtable_add(x", child_level.toString(), ", e", child_level.toString(), - "->string, ", - cJSON - .items - ?.getValue, - "(e", - child_level.toString(), - "), sizeof(", + "->string, validated, sizeof(", cJSON .items ?.cType, @@ -4985,13 +4978,31 @@ ${this.sourcelikeToString(cJSON.items?.deleteType ?? "")}(validated);`, type, false, ); + const container = + cJSON.cjsonType === "cJSON_Array" + ? { + create: ["list_create(false, NULL)"], + prefix: "list_add_tail(x->value, ", + } + : cJSON.cjsonType === "cJSON_Map" + ? { + create: [ + "hashtable_create(", + this.hashtableSize, + ", false)", + ], + prefix: "hashtable_add(x->value, e->string, ", + } + : undefined; + const items = cJSON.items; if ( - cJSON.cjsonType === "cJSON_Array" && - cJSON.items !== undefined + container !== undefined && + items !== undefined ) { - const items = cJSON.items; this.emitLine( - "x->value = list_create(false, NULL);", + "x->value = ", + container.create, + ";", ); this.emitBlock( ["if (NULL != x->value)"], @@ -5000,83 +5011,19 @@ ${this.sourcelikeToString(cJSON.items?.deleteType ?? "")}(validated);`, this.emitBlock( ["cJSON_ArrayForEach(e, j)"], () => { - const add = ( - cJSON: TypeCJSON, - ) => { - if ( - cJSON.items - ?.cjsonType === - "cJSON_Array" - ) { - /* Not supported */ - } else if ( - cJSON.items - ?.cjsonType === - "cJSON_Map" - ) { - /* Not supported */ - } else if ( - cJSON.items - ?.cjsonType === - "cJSON_Invalid" || - cJSON.items - ?.cjsonType === - "cJSON_NULL" - ) { - this.emitLine( - "list_add_tail(x->value, (", - cJSON.items?.cType, - " *)0xDEADBEAF, sizeof(", - cJSON.items?.cType, - " *));", - ); - } else if ( - cJSON.items - ?.cjsonType === - "cJSON_String" - ) { - this.emitLine( - "list_add_tail(x->value, strdup(", - cJSON.items - ?.getValue, - "(e)), sizeof(", - cJSON.items?.cType, - " *));", - ); - } else if ( - cJSON.items - ?.deleteType === - "cJSON_free" - ) { - this.emitLine( - cJSON.items.cType, - " * item = cJSON_malloc(sizeof(*item));", - ); - this.emitLine( - "if (NULL != item) { *item = ", - cJSON.items - .getValue, - "(e); list_add_tail(x->value, item, sizeof(*item)); }", - ); - } else { - this.emitLine( - "list_add_tail(x->value, ", - // @ts-expect-error awaiting refactor - cJSON.items - ?.getValue, - "(e), sizeof(", - cJSON.items?.cType, - " *));", - ); - } - }; - + /* A map's elements are checked too, nullable or not */ if ( - !items.isNullable && - items.isType !== "" + items.isType !== "" && + (cJSON.cjsonType === + "cJSON_Map" || + !items.isNullable) ) { this.emitLine( - "if (!", + "if (", + items.isNullable + ? "!cJSON_IsNull(e) && " + : "", + "!", items.isType, "(e)) { cJSON_Delete", className, @@ -5090,115 +5037,28 @@ ${this.sourcelikeToString(cJSON.items?.deleteType ?? "")}(validated);`, "if (!cJSON_IsNull(e))", ], () => { - add(cJSON); - }, - ); - this.emitBlock( - ["else"], - () => { - this.emitLine( - "list_add_tail(x->value, (void *)0xDEADBEEF, sizeof(void *));", + this.emitTopLevelAdd( + container.prefix, + items, + className, ); }, ); - } else { - add(cJSON); - } - }, - ); - }, - ); - } else if ( - cJSON.cjsonType === "cJSON_Map" && - cJSON.items !== undefined - ) { - this.emitLine( - "x->value = hashtable_create(", - this.hashtableSize, - ", false);", - ); - this.emitBlock( - ["if (NULL != x->value)"], - () => { - this.emitLine("cJSON * e = NULL;"); - this.emitBlock( - ["cJSON_ArrayForEach(e, j)"], - () => { - const add = ( - cJSON: TypeCJSON, - ) => { - if ( - cJSON.items - ?.cjsonType === - "cJSON_Array" - ) { - /* Not supported */ - } else if ( - cJSON.items - ?.cjsonType === - "cJSON_Map" - ) { - /* Not supported */ - } else if ( - cJSON.items - ?.cjsonType === - "cJSON_Invalid" || - cJSON.items - ?.cjsonType === - "cJSON_NULL" - ) { - this.emitLine( - "hashtable_add(x->value, e->string, (", - cJSON.items?.cType, - " *)0xDEADBEEF, sizeof(", - cJSON.items?.cType, - " *));", - ); - } else if ( - cJSON.items - ?.cjsonType === - "cJSON_String" - ) { - this.emitLine( - "hashtable_add(x->value, e->string, strdup(", - cJSON.items - ?.getValue, - "(e)), sizeof(", - cJSON.items?.cType, - " *));", - ); - } else { - this.emitLine( - "hashtable_add(x->value, e->string, ", - // @ts-expect-error awaiting refactor - cJSON.items - ?.getValue, - "(e), sizeof(", - cJSON.items?.cType, - " *));", - ); - } - }; - - if (cJSON.items?.isNullable) { - this.emitBlock( - [ - "if (!cJSON_IsNull(e))", - ], - () => { - add(cJSON); - }, - ); this.emitBlock( ["else"], () => { this.emitLine( - "hashtable_add(x->value, e->string, (void *)0xDEADBEEF, sizeof(void *));", + container.prefix, + "(void *)0xDEADBEEF, sizeof(void *));", ); }, ); } else { - add(cJSON); + this.emitTopLevelAdd( + container.prefix, + items, + className, + ); } }, ); @@ -5483,32 +5343,14 @@ ${this.sourcelikeToString(cJSON.items?.deleteType ?? "")}(validated);`, cJSON.cjsonType === "cJSON_Array" && cJSON.items !== undefined ) { + const items = cJSON.items; this.emitBlock(["if (NULL != x->value)"], () => { this.emitLine( - // @ts-expect-error awaiting refactor - cJSON.items?.cType, + items.cType, " * x1 = list_get_head(x->value);", ); this.emitBlock(["while (NULL != x1)"], () => { - if (cJSON.items?.isNullable) { - this.emitBlock( - ["if ((void *)0xDEADBEEF != x1)"], - () => { - this.emitLine( - // @ts-expect-error awaiting refactor - cJSON.items?.deleteType, - "(x1);", - ); - }, - ); - } else { - this.emitLine( - // @ts-expect-error awaiting refactor - cJSON.items?.deleteType, - "(x1);", - ); - } - + this.emitTopLevelDeleteItem(items, "x1"); this.emitLine("x1 = list_get_next(x->value);"); }); this.emitLine(cJSON.deleteType, "(x->value);"); @@ -5517,6 +5359,7 @@ ${this.sourcelikeToString(cJSON.items?.deleteType ?? "")}(validated);`, cJSON.cjsonType === "cJSON_Map" && cJSON.items !== undefined ) { + const items = cJSON.items; this.emitBlock(["if (NULL != x->value)"], () => { this.emitLine("char **keys = NULL;"); this.emitLine( @@ -5529,36 +5372,16 @@ ${this.sourcelikeToString(cJSON.items?.deleteType ?? "")}(validated);`, ], () => { this.emitLine( - // @ts-expect-error awaiting refactor - cJSON.items?.cType, + items.cType, " *x2 = hashtable_lookup(x->value, keys[index]);", ); this.emitBlock( ["if (NULL != x2)"], () => { - if (cJSON.items?.isNullable) { - this.emitBlock( - [ - "if ((", - cJSON.items?.cType, - " *)0xDEADBEEF != x2)", - ], - () => { - this.emitLine( - // @ts-expect-error awaiting refactor - cJSON.items - ?.deleteType, - "(x2);", - ); - }, - ); - } else { - this.emitLine( - // @ts-expect-error awaiting refactor - cJSON.items?.deleteType, - "(x2);", - ); - } + this.emitTopLevelDeleteItem( + items, + "x2", + ); }, ); }, @@ -5589,6 +5412,93 @@ ${this.sourcelikeToString(cJSON.items?.deleteType ?? "")}(validated);`, this.ensureBlankLine(); } + /** + * Adds the element `e` to a top-level list or map + * @param prefix: the call that adds it, up to the element + * @param items: the element's type + * @param className: the top-level's name + */ + private emitTopLevelAdd( + prefix: string, + items: TypeCJSON, + className: Name, + ): void { + if ( + items.cjsonType === "cJSON_Array" || + items.cjsonType === "cJSON_Map" + ) { + /* Not supported */ + } else if ( + items.cjsonType === "cJSON_Invalid" || + items.cjsonType === "cJSON_NULL" + ) { + this.emitLine( + prefix, + "(", + items.cType, + " *)0xDEADBEEF, sizeof(", + items.cType, + " *));", + ); + } else if (items.cjsonType === "cJSON_String") { + this.emitLine( + prefix, + "strdup(", + items.getValue, + "(e)), sizeof(", + items.cType, + " *));", + ); + } else if (items.deleteType === "cJSON_free") { + this.emitLine( + items.cType, + " * item = cJSON_malloc(sizeof(*item));", + ); + this.emitLine( + "if (NULL == item) { cJSON_Delete", + className, + "(x); return NULL; }", + ); + this.emitLine("*item = ", items.getValue, "(e);"); + this.emitLine(prefix, "item, sizeof(*item));"); + } else { + this.emitLine( + prefix, + items.getValue, + "(e), sizeof(", + items.cType, + " *));", + ); + } + } + + /** + * Deletes an element of a top-level list or map, as the delete + * function of a class deletes a list's or map's element: the marker + * 0xDEADBEEF of null is not freed + * @param items: the element's type + * @param variable: the variable that holds the element + */ + private emitTopLevelDeleteItem(items: TypeCJSON, variable: string): void { + if ( + items.cjsonType === "cJSON_Array" || + items.cjsonType === "cJSON_Map" || + items.cjsonType === "cJSON_Invalid" || + items.cjsonType === "cJSON_NULL" + ) { + /* Not supported, or nothing to do */ + } else if (items.isNullable) { + this.emitBlock( + ["if ((void *)0xDEADBEEF != ", variable, ")"], + () => { + this.emitLine(items.deleteType, "(", variable, ");"); + }, + ); + } else { + this.emitLine(items.deleteType, "(", variable, ");"); + } + } + /** * Convert quicktype type to cJSON type * @param t: quicktype type