diff --git a/README.md b/README.md index 9c7ed142c..d0ddb310d 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,8 @@ If you want more options, use `cJSON_ParseWithOpts(const char *value, const char If you want more options giving buffer length, use `cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated)`. +**Note on embedded NUL bytes:** `cJSON_ParseWithLength` (and `cJSON_ParseWithLengthOpts`) can parse JSON whose strings contain embedded NUL bytes, for example a string created from the input `"a\u0000b"`. However, cJSON stores every string as a NUL-terminated C string (`valuestring`) **without** keeping an explicit length. As a result, the embedded NUL is dropped when the value is later serialized: `cJSON_Print` / `cJSON_PrintUnformatted` rely on `strlen`, so they truncate the string at the first NUL. In other words, parse → print is **not** guaranteed to round-trip for strings containing embedded NUL bytes. If you need to preserve such data, decode the `\u0000` yourself or store the binary content encoded (e.g. base64) in the JSON. + ### Printing JSON Given a tree of `cJSON` items, you can print them as a string using `cJSON_Print`. diff --git a/cJSON.h b/cJSON.h index cab5feb42..cad9f6849 100644 --- a/cJSON.h +++ b/cJSON.h @@ -152,13 +152,19 @@ CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); /* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ /* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); +/* Parse a block of JSON that may not be null terminated. Note: cJSON stores strings as + * NUL-terminated C strings without an explicit length, so a string that contains an + * embedded NUL byte (e.g. parsed from "\u0000") will be truncated when printed. */ CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); /* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ /* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); -/* Render a cJSON entity to text for transfer/storage. */ +/* Render a cJSON entity to text for transfer/storage. + * NOTE: strings are stored as NUL-terminated C strings, so any string containing an + * embedded NUL byte (e.g. one parsed from "\u0000" via cJSON_ParseWithLength) is + * truncated at that NUL when printed, because the printer uses strlen. */ CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); /* Render a cJSON entity to text for transfer/storage without any formatting. */ CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); diff --git a/tests/parse_with_opts.c b/tests/parse_with_opts.c index e390b92de..a507e0346 100644 --- a/tests/parse_with_opts.c +++ b/tests/parse_with_opts.c @@ -97,6 +97,28 @@ static void parse_with_opts_should_parse_utf8_bom(void) cJSON_Delete(without_bom); } +/* Documents the known parse -> print limitation for strings that contain an + * embedded NUL byte (issue D from the static review): cJSON stores strings as + * NUL-terminated C strings without an explicit length, so cJSON_Print truncates + * at the first NUL instead of round-tripping the value. */ +static void parse_with_opts_should_truncate_embedded_null_on_print(void) +{ + const char input[] = "\"a\\u0000b\""; /* JSON text: "a\u0000b" */ + cJSON *item = cJSON_ParseWithLength(input, sizeof(input) - 1); + char *printed = NULL; + + TEST_ASSERT_NOT_NULL_MESSAGE(item, "Failed to parse string with embedded NUL."); + TEST_ASSERT_TRUE(cJSON_IsString(item)); + + printed = cJSON_PrintUnformatted(item); + TEST_ASSERT_NOT_NULL(printed); + /* strlen-based printing stops at the embedded NUL: only "a" survives */ + TEST_ASSERT_EQUAL_STRING("\"a\"", printed); + + free(printed); + cJSON_Delete(item); +} + int CJSON_CDECL main(void) { UNITY_BEGIN(); @@ -107,6 +129,7 @@ int CJSON_CDECL main(void) RUN_TEST(parse_with_opts_should_require_null_if_requested); RUN_TEST(parse_with_opts_should_return_parse_end); RUN_TEST(parse_with_opts_should_parse_utf8_bom); + RUN_TEST(parse_with_opts_should_truncate_embedded_null_on_print); return UNITY_END(); }