From 29752b71e3de1c36d39b6386c92c81a225b938e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:37:43 +0000 Subject: [PATCH 1/6] Initial plan From 73b8e74d441d1c4477d9b0498fafd5791bcf5bf2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:46:54 +0000 Subject: [PATCH 2/6] Harden parser length and allocation handling Co-authored-by: lcn2 <10429707+lcn2@users.noreply.github.com> --- jparse.l | 14 +++- jparse.lex.ref.h | 6 +- jparse.ref.c | 26 ++++--- jsemtblgen.c | 19 ++++-- json_parse.c | 132 +++++++++++++++++++++--------------- json_utf8.c | 4 +- json_utf8.h | 2 +- jstrdecode.c | 7 +- jstrencode.c | 7 +- test_jparse/Makefile | 25 ++++++- test_jparse/json_api_test.c | 114 +++++++++++++++++++++++++++++++ util.h | 29 ++++++++ verge.c | 15 +++- 13 files changed, 320 insertions(+), 80 deletions(-) create mode 100644 test_jparse/json_api_test.c diff --git a/jparse.l b/jparse.l index ae0207a..dd81df4 100644 --- a/jparse.l +++ b/jparse.l @@ -659,6 +659,16 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid) tree = json_alloc(JTYPE_UNSET); return tree; } + if (len > (size_t)INT_MAX) { + werr(40, __func__, "len: %zu > INT_MAX: %d", len, INT_MAX); + + /* + * yy_scan_bytes() accepts an int length, so oversized input cannot be scanned safely. + */ + *is_valid = false; + tree = json_alloc(JTYPE_UNSET); + return tree; + } /* * initialise scanner @@ -666,7 +676,7 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid) errno = 0; ret = jparse_lex_init_extra(&extra, &scanner); if (ret != 0) { - werrp(40, __func__, "jparse_lex_init_extra failed"); + werrp(41, __func__, "jparse_lex_init_extra failed"); /* * if jparse_lex_init_extra() reports an error (!= 0) then *is_valid @@ -687,7 +697,7 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid) * perhaps it should call err() instead but for now we make it a * non-fatal error as well. */ - werr(41, __func__, "unable to scan string"); + werr(42, __func__, "unable to scan string"); /* * since we cannot scan the bytes, we set *is_valid to false, even diff --git a/jparse.lex.ref.h b/jparse.lex.ref.h index ce6050d..3b4b545 100644 --- a/jparse.lex.ref.h +++ b/jparse.lex.ref.h @@ -56,9 +56,9 @@ #define jparse_HEADER_H 1 #define jparse_IN_HEADER 1 -#line 5 "jparse.lex.h" +#line 6 "jparse.lex.h" -#line 7 "jparse.lex.h" +#line 8 "jparse.lex.h" #define YY_INT_ALIGNED short int @@ -783,6 +783,6 @@ extern int yylex \ #line 357 "./jparse.l" -#line 732 "jparse.lex.h" +#line 733 "jparse.lex.h" #undef jparse_IN_HEADER #endif /* jparse_HEADER_H */ diff --git a/jparse.ref.c b/jparse.ref.c index 2dc06db..8ae30d3 100644 --- a/jparse.ref.c +++ b/jparse.ref.c @@ -52,9 +52,9 @@ * accordingly. */ #line 1 "./jparse.c" -#line 1 "jparse.c" +#line 2 "jparse.c" -#line 3 "jparse.c" +#line 4 "jparse.c" #define YY_INT_ALIGNED short int @@ -877,7 +877,7 @@ static YY_BUFFER_STATE bs; } \ } \ } -#line 826 "jparse.c" +#line 827 "jparse.c" /* * Section 1 - Patterns (regular expressions) and actions. */ @@ -947,7 +947,7 @@ static YY_BUFFER_STATE bs; * JSON_COMMA "," */ /* Actions. */ -#line 896 "jparse.c" +#line 897 "jparse.c" #define INITIAL 0 @@ -1229,7 +1229,7 @@ YY_DECL { #line 214 "./jparse.l" -#line 1178 "jparse.c" +#line 1179 "jparse.c" while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */ { @@ -1491,7 +1491,7 @@ YY_RULE_SETUP #line 357 "./jparse.l" YY_FATAL_ERROR( "flex scanner jammed" ); YY_BREAK -#line 1440 "jparse.c" +#line 1441 "jparse.c" case YY_STATE_EOF(INITIAL): yyterminate(); @@ -2959,6 +2959,16 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid) tree = json_alloc(JTYPE_UNSET); return tree; } + if (len > (size_t)INT_MAX) { + werr(40, __func__, "len: %zu > INT_MAX: %d", len, INT_MAX); + + /* + * yy_scan_bytes() accepts an int length, so oversized input cannot be scanned safely. + */ + *is_valid = false; + tree = json_alloc(JTYPE_UNSET); + return tree; + } /* * initialise scanner @@ -2966,7 +2976,7 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid) errno = 0; ret = jparse_lex_init_extra(&extra, &scanner); if (ret != 0) { - werrp(40, __func__, "jparse_lex_init_extra failed"); + werrp(41, __func__, "jparse_lex_init_extra failed"); /* * if jparse_lex_init_extra() reports an error (!= 0) then *is_valid @@ -2987,7 +2997,7 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid) * perhaps it should call err() instead but for now we make it a * non-fatal error as well. */ - werr(41, __func__, "unable to scan string"); + werr(42, __func__, "unable to scan string"); /* * since we cannot scan the bytes, we set *is_valid to false, even diff --git a/jsemtblgen.c b/jsemtblgen.c index 4089c15..d5098a4 100644 --- a/jsemtblgen.c +++ b/jsemtblgen.c @@ -843,6 +843,7 @@ alloc_c_funct_name(char const *prefix, char const *str) { bool prefix_is_reserved = false; /* true ==> prefix is a reserved word in C */ bool str_is_reserved = false; /* true ==> str is a reserved word in C */ + size_t alloc_len = 0; /* allocated size of ret including guard byte */ size_t len = 0; /* length of allocated string */ char *ret = NULL; /* allocated string to return */ char *p = NULL; /* next character to add */ @@ -860,13 +861,23 @@ alloc_c_funct_name(char const *prefix, char const *str) */ if (prefix != NULL) { prefix_is_reserved = test_reserved(prefix); - len = strlen(prefix) + (prefix_is_reserved ? 1 : 0) + 1; /* + 1 for _ after prefix */ + if (size_add(strlen(prefix), (prefix_is_reserved ? 1 : 0), &len) == false || + size_add(len, 1, &len) == false) { + err(20, __func__, "prefix length overflow"); + not_reached(); + } } str_is_reserved = test_reserved(str); - len += strlen(str) + (str_is_reserved ? 1 : 0) + 1; /* + 1 for NUL */ - ret = calloc(len + 1, sizeof(*ret)); /* + 1 for guard byte paranoia */ + if (size_add(len, strlen(str), &len) == false || + size_add(len, (str_is_reserved ? 1 : 0), &len) == false || + size_add(len, 1, &len) == false || + size_add(len, 1, &alloc_len) == false) { + err(20, __func__, "function name length overflow"); + not_reached(); + } + ret = calloc(alloc_len, sizeof(*ret)); /* + 1 for guard byte paranoia */ if (ret == NULL) { - errp(20, __func__, "calloc of %zu bytes failed", len); + errp(20, __func__, "calloc of %zu bytes failed", alloc_len); not_reached(); } p = ret; diff --git a/json_parse.c b/json_parse.c index 442891d..4db9617 100644 --- a/json_parse.c +++ b/json_parse.c @@ -227,18 +227,18 @@ json_encode(char const *ptr, size_t len, size_t *retlen, bool skip_quote) { char *ret = NULL; /* allocated encoding string or NULL */ char *beyond = NULL; /* beyond the end of the allocated encoding string */ - ssize_t mlen = 0; /* length of allocated encoded string */ + size_t alloc_len = 0; /* allocated size of ret including guard bytes */ + size_t mlen = 0; /* length of allocated encoded string */ char *p; /* next place to encode */ size_t i; /* * firewall */ + if (retlen != NULL) { + *retlen = 0; + } if (ptr == NULL) { - /* error - clear allocated length */ - if (retlen != NULL) { - *retlen = 0; - } warn(__func__, "called with NULL ptr"); return NULL; } @@ -247,27 +247,22 @@ json_encode(char const *ptr, size_t len, size_t *retlen, bool skip_quote) * count the bytes that will be in the encoded allocated string */ for (i=0; i < len; ++i) { - mlen += byte2asciistr[(uint8_t)(ptr[i])].len; - } - if (mlen < 0) { /* paranoia */ - /* error - clear allocated length */ - if (retlen != NULL) { - *retlen = 0; + if (size_add(mlen, byte2asciistr[(uint8_t)(ptr[i])].len, &mlen) == false) { + warn(__func__, "encoded length overflow for len: %zu", len); + return NULL; } - warn(__func__, "mlen #0: %zd < 0", mlen); - return NULL; } /* * calloc the encoded string */ - ret = calloc((size_t)mlen + 1 + 1, sizeof(*ret)); + if (size_add(mlen, 2, &alloc_len) == false) { + warn(__func__, "encoded allocation length overflow for len: %zu", len); + return NULL; + } + ret = calloc(alloc_len, sizeof(*ret)); if (ret == NULL) { - /* error - clear allocated length */ - if (retlen != NULL) { - *retlen = 0; - } - warn(__func__, "calloc of %zd bytes failed", (mlen + 1 + 1)); + warn(__func__, "calloc of %zu bytes failed", alloc_len); return NULL; } ret[mlen] = '\0'; /* terminate string */ @@ -308,23 +303,15 @@ json_encode(char const *ptr, size_t len, size_t *retlen, bool skip_quote) p += byte2asciistr[(uint8_t)(ptr[i])].len; } *p = '\0'; /* paranoia */ - mlen = p - ret; /* paranoia */ - if (mlen < 0) { /* paranoia */ - warn(__func__, "mlen #1: %zd < 0", mlen); - if (ret != NULL) { - free(ret); - ret = NULL; - } - return NULL; - } + mlen = (size_t)(p - ret); /* paranoia */ /* * return result */ - dbg(DBG_VVVHIGH, "returning from json_encode(ptr, %zu, *%zd, %s)", + dbg(DBG_VVVHIGH, "returning from json_encode(ptr, %zu, *%zu, %s)", len, mlen, booltostr(skip_quote)); if (retlen != NULL) { - *retlen = (size_t)mlen; + *retlen = mlen; } return ret; } @@ -1343,6 +1330,7 @@ chkbyte2asciistr(void) static char * decode_json_string(char const *ptr, size_t len, size_t mlen, size_t *retlen) { + size_t alloc_len = 0; /* allocated size of ret including guard bytes */ char *ret = NULL; /* allocated encoding string or NULL */ char *beyond = NULL; /* beyond the end of the allocated encoding string */ char *p = NULL; /* next place to encode */ @@ -1371,13 +1359,20 @@ decode_json_string(char const *ptr, size_t len, size_t mlen, size_t *retlen) /* * allocated decoded string */ - ret = calloc(mlen + 1 + 1, sizeof(*ret)); + if (size_add(mlen, 2, &alloc_len) == false) { + if (retlen != NULL) { + *retlen = 0; + } + warn(__func__, "decoded allocation length overflow for len: %zu", len); + return NULL; + } + ret = calloc(alloc_len, sizeof(*ret)); if (ret == NULL) { /* error - clear allocated length */ if (retlen != NULL) { *retlen = 0; } - warn(__func__, "calloc of %zu bytes failed", (mlen + 1 + 1)); + warn(__func__, "calloc of %zu bytes failed", alloc_len); return NULL; } ret[mlen] = '\0'; /* terminate string */ @@ -1678,16 +1673,15 @@ json_decode(char const *ptr, size_t len, bool quote, size_t *retlen) int32_t surrogate = 0; /* for surrogate pairs */ int scanned = 0; /* for sscanf() */ size_t i; - size_t bytes = 0; /* for utf8len() */ + ssize_t bytes = 0; /* for utf8len() */ /* * firewall */ + if (retlen != NULL) { + *retlen = 0; + } if (ptr == NULL) { - /* error - clear allocated length */ - if (retlen != NULL) { - *retlen = 0; - } warn(__func__, "called with NULL ptr"); return NULL; } @@ -1753,7 +1747,10 @@ json_decode(char const *ptr, size_t len, bool quote, size_t *retlen) * count a valid character */ default: - ++mlen; + if (size_add(mlen, 1, &mlen) == false) { + warn(__func__, "decoded length overflow for len: %zu", len); + return NULL; + } break; } @@ -1794,7 +1791,10 @@ json_decode(char const *ptr, size_t len, bool quote, size_t *retlen) /* * count \c escaped pair as 1 character */ - ++mlen; + if (size_add(mlen, 1, &mlen) == false) { + warn(__func__, "decoded length overflow for len: %zu", len); + return NULL; + } ++i; break; @@ -1834,8 +1834,11 @@ json_decode(char const *ptr, size_t len, bool quote, size_t *retlen) /* utf8len() already warns */ return NULL; } - dbg(DBG_VVHIGH, "UTF-8 bytes: %zu", bytes); - mlen += bytes; + dbg(DBG_VVHIGH, "UTF-8 bytes: %zd", bytes); + if (size_add(mlen, bytes, &mlen) == false) { + warn(__func__, "decoded length overflow for len: %zu", len); + return NULL; + } i += 5; } else if (scanned == 2) { /* @@ -1864,8 +1867,11 @@ json_decode(char const *ptr, size_t len, bool quote, size_t *retlen) /* utf8len() already warns */ return NULL; } - dbg(DBG_VVHIGH, "UTF-8 bytes: %zu", bytes); - mlen += bytes; + dbg(DBG_VVHIGH, "UTF-8 bytes: %zd", bytes); + if (size_add(mlen, bytes, &mlen) == false) { + warn(__func__, "decoded length overflow for len: %zu", len); + return NULL; + } /* * we skip 11 instead of 5 because 5 + LITLEN("\\uxxxx") is @@ -3214,6 +3220,7 @@ json_conv_number(char const *ptr, size_t len) { struct json *ret = NULL; /* JSON parser tree node to return */ struct json_number *item = NULL; /* JSON number item inside JSON parser tree node */ + size_t alloc_len = 0; /* allocated size of item->as_str including guard bytes */ bool decimal = false; /* true ==> ptr points to a base 10 integer in ASCII */ bool e_notation = false; /* true ==> ptr points to e notation in ASCII */ bool floating_notation = false; /* true ==> ptr points to floating point notation in ASCII */ @@ -3291,10 +3298,14 @@ json_conv_number(char const *ptr, size_t len) /* * duplicate the JSON integer string */ + if (size_add(len, 2, &alloc_len) == false) { + warn(__func__, "JSON number string length overflow: %zu", len); + return ret; + } errno = 0; /* pre-clear errno for errp() */ - item->as_str = calloc(len+1+1, sizeof(*(item->as_str))); + item->as_str = calloc(alloc_len, sizeof(*(item->as_str))); if (item->as_str == NULL) { - errp(13, __func__, "calloc #1 error allocating %zu bytes", (len+1+1)); + errp(13, __func__, "calloc #1 error allocating %zu bytes", alloc_len); not_reached(); } strncpy(item->as_str, ptr, len); @@ -3653,6 +3664,7 @@ json_conv_string(char const *ptr, size_t len, bool quote) { struct json *ret = NULL; /* JSON parser tree node to return */ struct json_string *item = NULL; /* JSON string item inside JSON parser tree node */ + size_t alloc_len = 0; /* allocated size of item->as_str including guard bytes */ /* * allocate an initialized JSON parse tree item @@ -3720,10 +3732,14 @@ json_conv_string(char const *ptr, size_t len, bool quote) /* * duplicate the JSON string */ + if (size_add(len, 2, &alloc_len) == false) { + warn(__func__, "JSON string length overflow: %zu", len); + return ret; + } errno = 0; /* pre-clear errno for errp() */ - item->as_str = calloc(len+1+1, sizeof(*(item->as_str))); + item->as_str = calloc(alloc_len, sizeof(*(item->as_str))); if (item->as_str == NULL) { - errp(17, __func__, "calloc #1 error allocating %zu bytes", (len+1+1)); + errp(17, __func__, "calloc #1 error allocating %zu bytes", alloc_len); not_reached(); } strncpy(item->as_str, ptr, len); @@ -3849,6 +3865,7 @@ json_conv_bool(char const *ptr, size_t len) { struct json *ret = NULL; /* JSON parser tree node to return */ struct json_boolean *item = NULL; /* allocated decoding string or NULL */ + size_t alloc_len = 0; /* allocated size of item->as_str including guard bytes */ /* * allocate an initialized JSON parse tree item @@ -3887,13 +3904,17 @@ json_conv_bool(char const *ptr, size_t len) /* * duplicate the JSON encoded string */ + if (size_add(len, 2, &alloc_len) == false) { + warn(__func__, "JSON boolean string length overflow: %zu", len); + return ret; + } errno = 0; /* pre-clear errno for errp() */ - item->as_str = calloc(len+1+1, sizeof(*(item->as_str))); + item->as_str = calloc(alloc_len, sizeof(*(item->as_str))); if (item->as_str == NULL) { - errp(20, __func__, "calloc #1 error allocating %zu bytes", (len+1+1)); + errp(20, __func__, "calloc #1 error allocating %zu bytes", alloc_len); not_reached(); } - memcpy(item->as_str, ptr, len+1); + memcpy(item->as_str, ptr, len); item->as_str[len] = '\0'; /* paranoia */ item->as_str[len+1] = '\0'; /* paranoia */ item->as_str_len = len; @@ -4004,6 +4025,7 @@ json_conv_null(char const *ptr, size_t len) { struct json *ret = NULL; /* JSON parser tree node to return */ struct json_null *item = NULL; /* allocated decoding string or NULL */ + size_t alloc_len = 0; /* allocated size of item->as_str including guard bytes */ /* * allocate an initialized JSON parse tree item @@ -4042,13 +4064,17 @@ json_conv_null(char const *ptr, size_t len) /* * duplicate the JSON string */ + if (size_add(len, 2, &alloc_len) == false) { + warn(__func__, "JSON null string length overflow: %zu", len); + return ret; + } errno = 0; /* pre-clear errno for errp() */ - item->as_str = calloc(len+1+1, sizeof(*(item->as_str))); + item->as_str = calloc(alloc_len, sizeof(*(item->as_str))); if (item->as_str == NULL) { - errp(23, __func__, "calloc #1 error allocating %zu bytes", (len+1+1)); + errp(23, __func__, "calloc #1 error allocating %zu bytes", alloc_len); not_reached(); } - memcpy(item->as_str, ptr, len+1); + memcpy(item->as_str, ptr, len); item->as_str[len] = '\0'; /* paranoia */ item->as_str[len+1] = '\0'; /* paranoia */ item->as_str_len = len; diff --git a/json_utf8.c b/json_utf8.c index 343d374..5ea72a8 100644 --- a/json_utf8.c +++ b/json_utf8.c @@ -62,7 +62,7 @@ * * NOTE: *str should point to the \u! */ -size_t +ssize_t utf8len(const char *str, int32_t surrogate) { unsigned char xa = 0; /* first hex digit */ @@ -70,7 +70,7 @@ utf8len(const char *str, int32_t surrogate) unsigned char xc = 0; /* third hex digit */ unsigned char xd = 0; /* fourth hex digit */ unsigned int x = 0; /* the hex value we attempt to extract */ - size_t len = 0; /* the number of bytes to return */ + ssize_t len = 0; /* the number of bytes to return */ int scanned = 0; /* how many values read */ if (str == NULL) { diff --git a/json_utf8.h b/json_utf8.h index 570d15b..4889253 100644 --- a/json_utf8.h +++ b/json_utf8.h @@ -70,7 +70,7 @@ */ #define JPARSE_UTF8_VERSION "2.1.4 2026-06-13" /* format: major.minor YYYY-MM-DD */ -extern size_t utf8len(const char *str, int32_t surrogate); +extern ssize_t utf8len(const char *str, int32_t surrogate); extern int32_t surrogate_pair_to_codepoint(int32_t hi, int32_t lo); extern int codepoint_to_unicode(char *str, unsigned int val); extern uint32_t utf8_to_codepoint(const char *str); diff --git a/jstrdecode.c b/jstrdecode.c index 3f4962b..947170b 100644 --- a/jstrdecode.c +++ b/jstrdecode.c @@ -194,6 +194,7 @@ add_decoded_string(char *string, size_t bufsiz) static char * dup_without_nl(char *input, size_t *inputlen) { + size_t alloc_len = 0; /* allocated size of dup_input */ char *dup_input = NULL; /* duplicate of input */ size_t i; size_t j; @@ -213,7 +214,11 @@ dup_without_nl(char *input, size_t *inputlen) /* * copy input removing all newlines */ - dup_input = calloc(*inputlen + 1, sizeof(*dup_input)); /* + 1 for guard NUL byte */ + if (size_add(*inputlen, 1, &alloc_len) == false) { + warn(__func__, "input length overflow: %zu", *inputlen); + return NULL; + } + dup_input = calloc(alloc_len, sizeof(*dup_input)); /* + 1 for guard NUL byte */ if (dup_input == NULL) { warn(__func__, "calloc of input failed"); return NULL; diff --git a/jstrencode.c b/jstrencode.c index 66140b7..da8937b 100644 --- a/jstrencode.c +++ b/jstrencode.c @@ -194,6 +194,7 @@ add_encoded_string(char *string, size_t bufsiz) static char * dup_without_nl(char *input, size_t *inputlen) { + size_t alloc_len = 0; /* allocated size of dup_input */ char *dup_input = NULL; /* duplicate of input */ size_t i; size_t j; @@ -213,7 +214,11 @@ dup_without_nl(char *input, size_t *inputlen) /* * copy input removing all newlines */ - dup_input = calloc(*inputlen + 1, sizeof(*dup_input)); /* + 1 for guard NUL byte */ + if (size_add(*inputlen, 1, &alloc_len) == false) { + warn(__func__, "input length overflow: %zu", *inputlen); + return NULL; + } + dup_input = calloc(alloc_len, sizeof(*dup_input)); /* + 1 for guard NUL byte */ if (dup_input == NULL) { warn(__func__, "calloc of input failed"); return NULL; diff --git a/test_jparse/Makefile b/test_jparse/Makefile index d5c373a..a9f2819 100644 --- a/test_jparse/Makefile +++ b/test_jparse/Makefile @@ -217,7 +217,7 @@ CFLAGS= ${C_STD} ${C_OPT} -pedantic ${WARN_FLAGS} ${C_SPECIAL} ${LDFLAGS} # source files that are permanent (not made, nor removed) # -C_SRC= jnum_chk.c jnum_gen.c jnum_header.c pr_jparse_test.c +C_SRC= jnum_chk.c jnum_gen.c jnum_header.c json_api_test.c pr_jparse_test.c H_SRC= jnum_chk.h jnum_gen.h # PICKY_OPTIONS= -c -e -s -t8 -u -v -w132 @@ -250,7 +250,7 @@ LIB_OBJS= # NOTE: ${OTHER_OBJS} are objects NOT put into a library and ARE removed by make clean # -OTHER_OBJS= jnum_gen.o jnum_chk.o jnum_test.o pr_jparse_test.o +OTHER_OBJS= jnum_gen.o jnum_chk.o jnum_test.o json_api_test.o pr_jparse_test.o # NOTE: intermediate files to make and removed by make clean # @@ -306,7 +306,7 @@ SH_TARGETS= # program targets to make by all, installed by install, and removed by clobber # -PROG_TARGETS= jnum_chk jnum_gen pr_jparse_test +PROG_TARGETS= jnum_chk jnum_gen json_api_test pr_jparse_test # what to make by all but NOT to removed by clobber # @@ -372,6 +372,12 @@ jnum_gen.o: jnum_gen.c jnum_gen.h ../json_utf8.h ../version.h jnum_gen: jnum_gen.o ../libjparse.a ${CC} ${CFLAGS} $^ -lm -o $@ ${LD_DIR2} -lpr -ldyn_array -ldbg +json_api_test.o: json_api_test.c ../json_utf8.h ../version.h + ${CC} ${CFLAGS} json_api_test.c -c + +json_api_test: json_api_test.o ../libjparse.a + ${CC} ${CFLAGS} $^ -o $@ ${LD_DIR2} -lpr -ldyn_array -ldbg + pr_jparse_test.o: pr_jparse_test.c pr_jparse_test.h ../json_utf8.h ../version.h ${CC} ${CFLAGS} pr_jparse_test.c -c @@ -444,7 +450,20 @@ test: echo "${OUR_NAME}: ERROR: executable not found: ./jnum_chk" 1>&2; \ echo "${OUR_NAME}: ERROR: unable to perform complete test" 1>&2; \ exit 1; \ + elif [[ ! -x ./json_api_test ]]; then \ + echo "${OUR_NAME}: ERROR: executable not found: ./json_api_test" 1>&2; \ + echo "${OUR_NAME}: ERROR: unable to perform complete test" 1>&2; \ + exit 1; \ else \ + echo "${OUR_NAME}: RUNNING: ./json_api_test"; \ + ./json_api_test; \ + EXIT_CODE="$$?"; \ + if [[ $$EXIT_CODE -ne 0 ]]; then \ + echo "${OUR_NAME}: ERROR: json_api_test failed, error code: $$EXIT_CODE"; \ + exit "$$EXIT_CODE"; \ + else \ + echo "${OUR_NAME}: PASSED: json_api_test"; \ + fi; \ echo "${OUR_NAME}: RUNNING: ./run_jparse_test.sh -D ${VERBOSITY} -v ${VERBOSITY} -j ./jparse -p test_jparse/pr_jparse_test -c test_jparse/jnum_chk"; \ ./run_jparse_tests.sh -D ${VERBOSITY} -v ${VERBOSITY} -j ./jparse -p test_jparse/pr_jparse_test -c test_jparse/jnum_chk; \ EXIT_CODE="$$?"; \ diff --git a/test_jparse/json_api_test.c b/test_jparse/json_api_test.c new file mode 100644 index 0000000..02dcedd --- /dev/null +++ b/test_jparse/json_api_test.c @@ -0,0 +1,114 @@ +/* + * json_api_test - focused API tests for json_parse helpers + * + * Copyright (c) 2026 by Landon Curt Noll and Cody Boone Ferguson. + * All Rights Reserved. + */ + +#include +#include +#include +#include +#include + +#include "../jparse.h" +#include "../json_util.h" + + +static void +free_tree(struct json *node) +{ + if (node != NULL) { + json_tree_free(node, JSON_INFINITE_DEPTH); + free(node); + } +} + + +static int +test_malformed_unicode(void) +{ + char const malformed[] = "\\u12xz"; + char *decoded = NULL; + size_t retlen = 123; + + decoded = json_decode(malformed, strlen(malformed), false, &retlen); + if (decoded != NULL) { + fprintf(stderr, "json_decode() unexpectedly decoded malformed unicode escape\n"); + free(decoded); + return 1; + } + if (retlen != 0) { + fprintf(stderr, "json_decode() did not clear retlen on malformed unicode escape\n"); + return 1; + } + return 0; +} + + +static int +test_bool_slice(void) +{ + char const bool_buf[] = { 't', 'r', 'u', 'e', 'x' }; + struct json *node = NULL; + + node = json_conv_bool(bool_buf, 4); + if (node == NULL || node->type != JTYPE_BOOL || node->item.boolean.converted == false || + node->item.boolean.parsed == false || node->item.boolean.value == false || + node->item.boolean.as_str == NULL || strcmp(node->item.boolean.as_str, "true") != 0) { + fprintf(stderr, "json_conv_bool() failed to parse an exact-length true slice\n"); + free_tree(node); + return 1; + } + free_tree(node); + return 0; +} + + +static int +test_null_slice(void) +{ + char const null_buf[] = { 'n', 'u', 'l', 'l', 'x' }; + struct json *node = NULL; + + node = json_conv_null(null_buf, 4); + if (node == NULL || node->type != JTYPE_NULL || node->item.null.converted == false || + node->item.null.parsed == false || node->item.null.as_str == NULL || + strcmp(node->item.null.as_str, "null") != 0) { + fprintf(stderr, "json_conv_null() failed to parse an exact-length null slice\n"); + free_tree(node); + return 1; + } + free_tree(node); + return 0; +} + + +static int +test_parse_json_len_limit(void) +{ + struct json *tree = NULL; + bool is_valid = true; + + tree = parse_json("{}", (size_t)INT_MAX + 1U, NULL, &is_valid); + if (tree == NULL || is_valid != false || tree->type != JTYPE_UNSET) { + fprintf(stderr, "parse_json() failed to reject len > INT_MAX safely\n"); + free_tree(tree); + return 1; + } + free_tree(tree); + return 0; +} + + +int +main(void) +{ + int ret = 0; + + ret |= test_malformed_unicode(); + ret |= test_bool_slice(); + ret |= test_null_slice(); + ret |= test_parse_json_len_limit(); + return ret; +} diff --git a/util.h b/util.h index 52ac7ea..c49a27f 100644 --- a/util.h +++ b/util.h @@ -164,6 +164,35 @@ #define is_all_whitespace(buf, len) (find_text((buf), (len), NULL) == 0) #define is_all_whitespace_str(str) (is_all_whitespace((str), strlen(str))) +/* + * size_t overflow helpers + */ +static inline bool +size_add(size_t a, size_t b, size_t *sum) +{ + if (sum == NULL) { + return false; + } + if (a > SIZE_MAX - b) { + return false; + } + *sum = a + b; + return true; +} + +static inline bool +size_mul(size_t a, size_t b, size_t *product) +{ + if (product == NULL) { + return false; + } + if (a > 0 && b > SIZE_MAX / a) { + return false; + } + *product = a * b; + return true; +} + /* * non-strict floating match to 1 part in MATCH_PRECISION */ diff --git a/verge.c b/verge.c index 824b7b0..ff5a7bf 100644 --- a/verge.c +++ b/verge.c @@ -238,6 +238,8 @@ vercmp(char *ver1, char *ver2) size_t allocate_vers(char *str, intmax_t **pvers) { + size_t array_len = 0; /* number of intmax_t values to allocate */ + size_t alloc_size = 0; /* size in bytes of *pvers allocation */ char *wstr = NULL; /* working allocated copy of orig_str */ char *wstr_start = NULL; /* pointer to starting point of wstr */ size_t len; /* length of version string */ @@ -359,10 +361,19 @@ allocate_vers(char *str, intmax_t **pvers) * * Allocate the array of dot_count+1 versions. */ + if (size_add(dot_count, 2, &array_len) == false || + size_mul(array_len, sizeof(**pvers), &alloc_size) == false) { + warn(__func__, "version component allocation overflow for: <%s>", wstr); + if (wstr_start != NULL) { + free(wstr_start); + wstr_start = NULL; + } + return 0; + } errno = 0; /* pre-clear errno for errp() */ - *pvers = (intmax_t *)calloc(dot_count+1+1, sizeof(**pvers)); + *pvers = (intmax_t *)calloc(array_len, sizeof(**pvers)); if (*pvers == NULL) { - errp(59, __func__, "cannot calloc %zu intmax_ts", dot_count+1+1); + errp(59, __func__, "cannot calloc %zu bytes", alloc_size); not_reached(); } From aba4979a5b1e6063c4e121ce90cd090ff5a36f91 Mon Sep 17 00:00:00 2001 From: Landon Curt Noll Date: Fri, 25 Sep 2026 19:04:24 -0700 Subject: [PATCH 3/6] Update length check for INT_MAX in jparse.l Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- jparse.l | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jparse.l b/jparse.l index dd81df4..8ff2576 100644 --- a/jparse.l +++ b/jparse.l @@ -659,8 +659,8 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid) tree = json_alloc(JTYPE_UNSET); return tree; } - if (len > (size_t)INT_MAX) { - werr(40, __func__, "len: %zu > INT_MAX: %d", len, INT_MAX); +if (len > (size_t)(INT_MAX - 2)) { + werr(40, __func__, "len: %zu > INT_MAX - 2: %d", len, INT_MAX - 2); /* * yy_scan_bytes() accepts an int length, so oversized input cannot be scanned safely. From 526440376bd6b639ef5680d6aab6f0384325fb18 Mon Sep 17 00:00:00 2001 From: Landon Curt Noll Date: Fri, 25 Sep 2026 19:05:05 -0700 Subject: [PATCH 4/6] Update error handling for length checks Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- jparse.ref.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jparse.ref.c b/jparse.ref.c index 8ae30d3..5abed57 100644 --- a/jparse.ref.c +++ b/jparse.ref.c @@ -2959,8 +2959,8 @@ parse_json(char const *ptr, size_t len, char const *filename, bool *is_valid) tree = json_alloc(JTYPE_UNSET); return tree; } - if (len > (size_t)INT_MAX) { - werr(40, __func__, "len: %zu > INT_MAX: %d", len, INT_MAX); +if (len > (size_t)(INT_MAX - 2)) { + werr(40, __func__, "len: %zu > INT_MAX - 2: %d", len, INT_MAX - 2); /* * yy_scan_bytes() accepts an int length, so oversized input cannot be scanned safely. From f4d8f6f8223542eaf5dedb46851fd6aa7e98096f Mon Sep 17 00:00:00 2001 From: Landon Curt Noll Date: Fri, 25 Sep 2026 19:05:36 -0700 Subject: [PATCH 5/6] Link math library in json_api_test target Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- test_jparse/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_jparse/Makefile b/test_jparse/Makefile index a9f2819..23d99ee 100644 --- a/test_jparse/Makefile +++ b/test_jparse/Makefile @@ -376,7 +376,7 @@ json_api_test.o: json_api_test.c ../json_utf8.h ../version.h ${CC} ${CFLAGS} json_api_test.c -c json_api_test: json_api_test.o ../libjparse.a - ${CC} ${CFLAGS} $^ -o $@ ${LD_DIR2} -lpr -ldyn_array -ldbg + ${CC} ${CFLAGS} $^ -lm -o $@ ${LD_DIR2} -lpr -ldyn_array -ldbg pr_jparse_test.o: pr_jparse_test.c pr_jparse_test.h ../json_utf8.h ../version.h ${CC} ${CFLAGS} pr_jparse_test.c -c From 5d73402ecb08715b8365da0f1e863c8dc4da5ed9 Mon Sep 17 00:00:00 2001 From: Landon Curt Noll Date: Fri, 25 Sep 2026 19:32:58 -0700 Subject: [PATCH 6/6] prep for PR with version, .gitignore, clobber and parser mods --- .gitignore | 13 +++++----- CHANGES.md | 58 ++++++++++++++++++++++++++++++++++++++++++++ jparse.l | 38 ++++++++++++++--------------- jparse.lex.ref.h | 6 ++--- jparse.ref.c | 50 +++++++++++++++++++------------------- jparse.y | 2 +- jsemtblgen.c | 30 +++++++++++------------ test_jparse/Makefile | 4 ++- version.h | 8 +++--- 9 files changed, 135 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 790bc95..a37289e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,11 @@ # We sort the list below via: sort -d -u # -.DS_Store -/Makefile.local -/Makefile.orig *.[ao] /backup /bug-report* *build.log* /c.tmpfile.*.c +.DS_Store *.dSYM/ foobar /jfmt @@ -39,12 +37,12 @@ jparse_bug_report.*.c /lex.jparse_.c /.local.dir.tags /makefile.local +/Makefile.local +/Makefile.orig /.sorry.*.c -.*.sw[a-zA-Z0-9]* .sw* +.*.sw[a-zA-Z0-9]* /tags -/test_jparse/Makefile.local -/test_jparse/Makefile.orig /test_jparse/c.tmpfile.*.c /test_jparse/jnum_chk /test_jparse/jnum_chk-*.tmp @@ -53,10 +51,13 @@ jparse_bug_report.*.c /test_jparse/jnum_test-*.tmp /test_jparse/jparse_test.log /test_jparse/.jparse_test.stderr.* +test_jparse/json_api_test /test_jparse/jstr_test2.out /test_jparse/jstr_test.out /test_jparse/.local.dir.tags /test_jparse/makefile.local +/test_jparse/Makefile.local +/test_jparse/Makefile.orig /test_jparse/print_test /test_jparse/pr_jparse_test /test_jparse/pr_jparse_test-*.tmp diff --git a/CHANGES.md b/CHANGES.md index d435d5b..554943b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,63 @@ # Significant changes in the JSON parser repo + +## Release 2.6.0 2026-09-25 + +Performed a major code audit. + +Harden parser allocation and length handling across JSON decode and scanner entry points + +This PR audits the parser and supporting utilities for high-confidence +memory-safety and logic issues, with emphasis on allocation sizing, decode +paths, scanner length boundaries, and exact-length buffer handling. The +changes target overflow/OOB conditions in public and internal APIs while +preserving existing parser behavior for valid inputs. + +- **JSON decode and encode bounds** + - Added checked `size_t` helpers for additive/multiplicative allocation math. + - Guarded encoded/decoded length accumulation before allocation and before buffer growth assumptions. + - Hardened decode buffer allocation against wrapped `mlen + 2` sizing. + +- **Malformed `\u` escape handling** + - Changed `utf8len()` to return a signed length so malformed escapes propagate failure instead of wrapping to `SIZE_MAX`. + - Updated JSON decode paths to reject invalid UTF-8 escape length results before folding them into decoded-size calculations. + +- **Scanner length boundary** + - Added an explicit `INT_MAX` guard before passing caller-controlled lengths into flex `yy_scan_bytes()`. + - Oversized inputs now fail safely instead of truncating through the scanner’s signed-length boundary. + +- **Exact-length API correctness** + - Fixed `json_conv_bool()` and `json_conv_null()` to copy exactly `len` bytes and terminate locally. + - This removes the implicit read of `len + 1` bytes from caller-provided slices that are not required to be NUL-terminated. + +- **Overflow hardening in utility paths** + - Applied checked allocation sizing to helper paths that duplicate or build derived strings + (`jstrencode`, `jstrdecode`, `jsemtblgen`, `verge`). + - Synced the scanner source change into the checked-in flex reference files used by non-regenerated builds. + +- **Focused regression coverage** + - Added a small API-level regression test binary covering: + - malformed Unicode escapes + - exact-length boolean/null slices + - oversized `parse_json()` length rejection + +Example of the exact-length fix: + +```c + item->as_str = calloc(alloc_len, sizeof(*(item->as_str))); + memcpy(item->as_str, ptr, len); + item->as_str[len] = '\0'; +``` + +In theory, the normal execution of this code should be identical to +"Release 2.5.11 2026-08-03", except in extreme or unusual cases. + +Updated `JPARSE_REPO_VERSION` to `"2.6.0 2026-09-25"`. +Updated `JPARSE_TOOL_VERSION` to `"2.1.0 2026-09-25"`. +Updated `JPARSE_LIBRARY_VERSION` to `"2.5.0 2026-09-25"`. +Updated `JPARSE_UTILS_VERSION` to `"2.2.0 2026-06-25"`. + + ## Release 2.5.11 2026-08-03 Make jval.c `#include "jval.h"` rather than `jparse_main.h`. The code is diff --git a/jparse.l b/jparse.l index 8ff2576..f76b5d9 100644 --- a/jparse.l +++ b/jparse.l @@ -839,7 +839,7 @@ parse_json_str(char const *ptr, size_t len, bool *is_valid) * firewall */ if (is_valid == NULL) { - err(42, __func__, "is_valid == NULL"); + err(43, __func__, "is_valid == NULL"); not_reached(); } else { /* @@ -854,7 +854,7 @@ parse_json_str(char const *ptr, size_t len, bool *is_valid) * firewall */ if (ptr == NULL) { - werr(43, __func__, "ptr is NULL"); + werr(44, __func__, "ptr is NULL"); /* * flag that we have invalid JSON @@ -919,7 +919,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) * firewall */ if (is_valid == NULL) { - err(44, __func__, "is_valid == NULL"); + err(45, __func__, "is_valid == NULL"); not_reached(); } else { /* @@ -943,7 +943,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) /* * report NULL stream */ - werr(45, __func__, "stream is NULL"); + werr(46, __func__, "stream is NULL"); /* * flag that we have invalid JSON @@ -965,7 +965,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) /* * report closed stream */ - werr(46, __func__, "stream is not open"); + werr(47, __func__, "stream is not open"); /* * flag that we have invalid JSON @@ -988,7 +988,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) /* * warn about read error */ - werr(47, __func__, "could not read read stream"); + werr(48, __func__, "could not read read stream"); /* * we need to clearerr() or fclose(), depending on the stream. */ @@ -1014,14 +1014,14 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) * report invalid bytes */ if (low_bytes > 0 && nul_bytes > 0) { - werr(48, __func__, "%zu low byte%s and %zu NUL byte%s detected: data block is NOT valid JSON", + werr(49, __func__, "%zu low byte%s and %zu NUL byte%s detected: data block is NOT valid JSON", low_bytes, low_bytes > 1 ? "s":"", nul_bytes, nul_bytes > 1 ? "s":""); } else if (low_bytes > 0) { - werr(49, __func__, "%zu low byte%s detected: data block is NOT valid JSON", + werr(50, __func__, "%zu low byte%s detected: data block is NOT valid JSON", low_bytes, low_bytes > 1 ? "s":""); } else if (nul_bytes > 0) { - werr(50, __func__, "%zu NUL byte%s detected: data block is NOT valid JSON", + werr(51, __func__, "%zu NUL byte%s detected: data block is NOT valid JSON", nul_bytes, nul_bytes > 1 ? "s":""); } @@ -1102,7 +1102,7 @@ exists(char const *path) * firewall */ if (path == NULL) { - err(51, __func__, "called with NULL path"); + err(52, __func__, "called with NULL path"); not_reached(); } @@ -1143,7 +1143,7 @@ is_file(char const *path) * firewall */ if (path == NULL) { - err(52, __func__, "called with NULL path"); + err(53, __func__, "called with NULL path"); not_reached(); } @@ -1194,7 +1194,7 @@ is_read(char const *path) * firewall */ if (path == NULL) { - err(53, __func__, "called with NULL path"); + err(54, __func__, "called with NULL path"); not_reached(); } @@ -1254,7 +1254,7 @@ parse_json_file(char const *filename, bool *is_valid) * firewall */ if (is_valid == NULL) { - err(54, __func__, "is_valid == NULL"); + err(55, __func__, "is_valid == NULL"); not_reached(); } else { /* @@ -1265,7 +1265,7 @@ parse_json_file(char const *filename, bool *is_valid) *is_valid = true; } if (filename == NULL) { - werr(55, __func__, "passed NULL filename"); + werr(56, __func__, "passed NULL filename"); /* * flag that we have invalid JSON @@ -1283,7 +1283,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * warn about bogus filename */ - werr(56, __func__, "passed empty filename"); + werr(57, __func__, "passed empty filename"); /* * flag that we have invalid JSON @@ -1315,7 +1315,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * report missing file */ - werr(57, __func__, "passed filename that's not actually a file: %s", filename); + werr(58, __func__, "passed filename that's not actually a file: %s", filename); /* * flag that we have invalid JSON @@ -1333,7 +1333,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * report that file is not a normal file */ - werr(58, __func__, "passed filename not a normal file: %s", filename); + werr(59, __func__, "passed filename not a normal file: %s", filename); /* * report invalid JSON @@ -1350,7 +1350,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * report unreadable file */ - werr(59, __func__, "passed filename not a readable file: %s", filename); + werr(60, __func__, "passed filename not a readable file: %s", filename); /* * flag that we have invalid JSON @@ -1374,7 +1374,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * warn about file open error */ - werrp(60, __func__, "couldn't open file %s, ignoring", filename); + werrp(61, __func__, "couldn't open file %s, ignoring", filename); /* * flag that we have invalid JSON diff --git a/jparse.lex.ref.h b/jparse.lex.ref.h index 3b4b545..ce6050d 100644 --- a/jparse.lex.ref.h +++ b/jparse.lex.ref.h @@ -56,9 +56,9 @@ #define jparse_HEADER_H 1 #define jparse_IN_HEADER 1 -#line 6 "jparse.lex.h" +#line 5 "jparse.lex.h" -#line 8 "jparse.lex.h" +#line 7 "jparse.lex.h" #define YY_INT_ALIGNED short int @@ -783,6 +783,6 @@ extern int yylex \ #line 357 "./jparse.l" -#line 733 "jparse.lex.h" +#line 732 "jparse.lex.h" #undef jparse_IN_HEADER #endif /* jparse_HEADER_H */ diff --git a/jparse.ref.c b/jparse.ref.c index 5abed57..febacab 100644 --- a/jparse.ref.c +++ b/jparse.ref.c @@ -52,9 +52,9 @@ * accordingly. */ #line 1 "./jparse.c" -#line 2 "jparse.c" +#line 1 "jparse.c" -#line 4 "jparse.c" +#line 3 "jparse.c" #define YY_INT_ALIGNED short int @@ -877,7 +877,7 @@ static YY_BUFFER_STATE bs; } \ } \ } -#line 827 "jparse.c" +#line 826 "jparse.c" /* * Section 1 - Patterns (regular expressions) and actions. */ @@ -947,7 +947,7 @@ static YY_BUFFER_STATE bs; * JSON_COMMA "," */ /* Actions. */ -#line 897 "jparse.c" +#line 896 "jparse.c" #define INITIAL 0 @@ -1229,7 +1229,7 @@ YY_DECL { #line 214 "./jparse.l" -#line 1179 "jparse.c" +#line 1178 "jparse.c" while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */ { @@ -1491,7 +1491,7 @@ YY_RULE_SETUP #line 357 "./jparse.l" YY_FATAL_ERROR( "flex scanner jammed" ); YY_BREAK -#line 1441 "jparse.c" +#line 1440 "jparse.c" case YY_STATE_EOF(INITIAL): yyterminate(); @@ -3139,7 +3139,7 @@ parse_json_str(char const *ptr, size_t len, bool *is_valid) * firewall */ if (is_valid == NULL) { - err(42, __func__, "is_valid == NULL"); + err(43, __func__, "is_valid == NULL"); not_reached(); } else { /* @@ -3154,7 +3154,7 @@ parse_json_str(char const *ptr, size_t len, bool *is_valid) * firewall */ if (ptr == NULL) { - werr(43, __func__, "ptr is NULL"); + werr(44, __func__, "ptr is NULL"); /* * flag that we have invalid JSON @@ -3219,7 +3219,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) * firewall */ if (is_valid == NULL) { - err(44, __func__, "is_valid == NULL"); + err(45, __func__, "is_valid == NULL"); not_reached(); } else { /* @@ -3243,7 +3243,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) /* * report NULL stream */ - werr(45, __func__, "stream is NULL"); + werr(46, __func__, "stream is NULL"); /* * flag that we have invalid JSON @@ -3265,7 +3265,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) /* * report closed stream */ - werr(46, __func__, "stream is not open"); + werr(47, __func__, "stream is not open"); /* * flag that we have invalid JSON @@ -3288,7 +3288,7 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) /* * warn about read error */ - werr(47, __func__, "could not read read stream"); + werr(48, __func__, "could not read read stream"); /* * we need to clearerr() or fclose(), depending on the stream. */ @@ -3314,14 +3314,14 @@ parse_json_stream(FILE *stream, char const *filename, bool *is_valid) * report invalid bytes */ if (low_bytes > 0 && nul_bytes > 0) { - werr(48, __func__, "%zu low byte%s and %zu NUL byte%s detected: data block is NOT valid JSON", + werr(49, __func__, "%zu low byte%s and %zu NUL byte%s detected: data block is NOT valid JSON", low_bytes, low_bytes > 1 ? "s":"", nul_bytes, nul_bytes > 1 ? "s":""); } else if (low_bytes > 0) { - werr(49, __func__, "%zu low byte%s detected: data block is NOT valid JSON", + werr(50, __func__, "%zu low byte%s detected: data block is NOT valid JSON", low_bytes, low_bytes > 1 ? "s":""); } else if (nul_bytes > 0) { - werr(50, __func__, "%zu NUL byte%s detected: data block is NOT valid JSON", + werr(51, __func__, "%zu NUL byte%s detected: data block is NOT valid JSON", nul_bytes, nul_bytes > 1 ? "s":""); } @@ -3402,7 +3402,7 @@ exists(char const *path) * firewall */ if (path == NULL) { - err(51, __func__, "called with NULL path"); + err(52, __func__, "called with NULL path"); not_reached(); } @@ -3443,7 +3443,7 @@ is_file(char const *path) * firewall */ if (path == NULL) { - err(52, __func__, "called with NULL path"); + err(53, __func__, "called with NULL path"); not_reached(); } @@ -3494,7 +3494,7 @@ is_read(char const *path) * firewall */ if (path == NULL) { - err(53, __func__, "called with NULL path"); + err(54, __func__, "called with NULL path"); not_reached(); } @@ -3554,7 +3554,7 @@ parse_json_file(char const *filename, bool *is_valid) * firewall */ if (is_valid == NULL) { - err(54, __func__, "is_valid == NULL"); + err(55, __func__, "is_valid == NULL"); not_reached(); } else { /* @@ -3565,7 +3565,7 @@ parse_json_file(char const *filename, bool *is_valid) *is_valid = true; } if (filename == NULL) { - werr(55, __func__, "passed NULL filename"); + werr(56, __func__, "passed NULL filename"); /* * flag that we have invalid JSON @@ -3583,7 +3583,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * warn about bogus filename */ - werr(56, __func__, "passed empty filename"); + werr(57, __func__, "passed empty filename"); /* * flag that we have invalid JSON @@ -3615,7 +3615,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * report missing file */ - werr(57, __func__, "passed filename that's not actually a file: %s", filename); + werr(58, __func__, "passed filename that's not actually a file: %s", filename); /* * flag that we have invalid JSON @@ -3633,7 +3633,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * report that file is not a normal file */ - werr(58, __func__, "passed filename not a normal file: %s", filename); + werr(59, __func__, "passed filename not a normal file: %s", filename); /* * report invalid JSON @@ -3650,7 +3650,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * report unreadable file */ - werr(59, __func__, "passed filename not a readable file: %s", filename); + werr(60, __func__, "passed filename not a readable file: %s", filename); /* * flag that we have invalid JSON @@ -3674,7 +3674,7 @@ parse_json_file(char const *filename, bool *is_valid) /* * warn about file open error */ - werrp(60, __func__, "couldn't open file %s, ignoring", filename); + werrp(61, __func__, "couldn't open file %s, ignoring", filename); /* * flag that we have invalid JSON diff --git a/jparse.y b/jparse.y index c1c9507..f613209 100644 --- a/jparse.y +++ b/jparse.y @@ -964,7 +964,7 @@ yyerror(JPARSE_LTYPE *yyltype, struct json **node, yyscan_t scanner, char const * firewall */ if (scanner == NULL) { - err(61, __func__, "NULL scanner"); + err(62, __func__, "NULL scanner"); not_reached(); } /* diff --git a/jsemtblgen.c b/jsemtblgen.c index d5098a4..a25b0c1 100644 --- a/jsemtblgen.c +++ b/jsemtblgen.c @@ -872,12 +872,12 @@ alloc_c_funct_name(char const *prefix, char const *str) size_add(len, (str_is_reserved ? 1 : 0), &len) == false || size_add(len, 1, &len) == false || size_add(len, 1, &alloc_len) == false) { - err(20, __func__, "function name length overflow"); + err(21, __func__, "function name length overflow"); not_reached(); } ret = calloc(alloc_len, sizeof(*ret)); /* + 1 for guard byte paranoia */ if (ret == NULL) { - errp(20, __func__, "calloc of %zu bytes failed", alloc_len); + errp(22, __func__, "calloc of %zu bytes failed", alloc_len); not_reached(); } p = ret; @@ -978,11 +978,11 @@ append_unique_str(struct dyn_array *tbl, char *str) * firewall */ if (tbl == NULL) { - err(21, __func__, "tbl is NULL"); + err(23, __func__, "tbl is NULL"); not_reached(); } if (str == NULL) { - err(22, __func__, "str is NULL"); + err(24, __func__, "str is NULL"); not_reached(); } @@ -1001,7 +1001,7 @@ append_unique_str(struct dyn_array *tbl, char *str) /* get next string pointer */ u = dyn_array_value(tbl, char *, i); if (u == NULL) { /* paranoia */ - err(23, __func__, "found NULL pointer in function name dynamic array element: %ju", (uintmax_t)i); + err(25, __func__, "found NULL pointer in function name dynamic array element: %ju", (uintmax_t)i); not_reached(); } @@ -1042,15 +1042,15 @@ print_sem_c_src(struct dyn_array *tbl, char *tbl_name, char *cap_tbl_name) * firewall */ if (tbl == NULL) { - err(24, __func__, "tbl is NULL"); + err(26, __func__, "tbl is NULL"); not_reached(); } if (tbl_name == NULL) { - err(25, __func__, "tbl_name is NULL"); + err(27, __func__, "tbl_name is NULL"); not_reached(); } if (cap_tbl_name == NULL) { - err(26, __func__, "cap_tbl_name is NULL"); + err(28, __func__, "cap_tbl_name is NULL"); not_reached(); } @@ -1258,15 +1258,15 @@ print_sem_h_src(struct dyn_array *tbl, char *tbl_name, char *cap_tbl_name) * firewall */ if (tbl == NULL) { - err(27, __func__, "tbl is NULL"); + err(29, __func__, "tbl is NULL"); not_reached(); } if (tbl_name == NULL) { - err(28, __func__, "tbl_name is NULL"); + err(30, __func__, "tbl_name is NULL"); not_reached(); } if (cap_tbl_name == NULL) { - err(29, __func__, "cap_tbl_name is NULL"); + err(31, __func__, "cap_tbl_name is NULL"); not_reached(); } @@ -1280,7 +1280,7 @@ print_sem_h_src(struct dyn_array *tbl, char *tbl_name, char *cap_tbl_name) */ unique_tbl = dyn_array_create(sizeof(char *), CHUNK, CHUNK, true); if (unique_tbl == NULL) { - err(30, __func__, "dyn_array_create failed"); + err(32, __func__, "dyn_array_create failed"); not_reached(); } @@ -1362,7 +1362,7 @@ print_sem_h_src(struct dyn_array *tbl, char *tbl_name, char *cap_tbl_name) /* form function name based on -M member_func */ func_name = alloc_c_funct_name(prefix, member_func); if (func_name == NULL) { - err(31, __func__, "alloc_c_funct_name for member_func returned NULL"); + err(33, __func__, "alloc_c_funct_name for member_func returned NULL"); not_reached(); } is_unique = append_unique_str(unique_tbl, func_name); @@ -1389,7 +1389,7 @@ print_sem_h_src(struct dyn_array *tbl, char *tbl_name, char *cap_tbl_name) /* form function name based on JSON member name */ func_name = alloc_c_funct_name(prefix, p->name); if (func_name == NULL) { - err(32, __func__, "alloc_c_funct_name for JSON member name returned NULL"); + err(34, __func__, "alloc_c_funct_name for JSON member name returned NULL"); not_reached(); } is_unique = append_unique_str(unique_tbl, func_name); @@ -1427,7 +1427,7 @@ print_sem_h_src(struct dyn_array *tbl, char *tbl_name, char *cap_tbl_name) /* form function name based validation function */ func_name = alloc_c_funct_name(prefix, validate); if (func_name == NULL) { - err(33, __func__, "alloc_c_funct_name for validation function name returned NULL"); + err(35, __func__, "alloc_c_funct_name for validation function name returned NULL"); not_reached(); } is_unique = append_unique_str(unique_tbl, func_name); diff --git a/test_jparse/Makefile b/test_jparse/Makefile index 23d99ee..9a00119 100644 --- a/test_jparse/Makefile +++ b/test_jparse/Makefile @@ -704,7 +704,7 @@ clobber: legacy_clobber clean ${S} echo "${OUR_NAME}: make $@ starting" ${S} echo ${Q} ${RM} ${RM_V} -f ${TARGETS} - ${Q} ${RM} ${RM_V} -f jparse_test.log + ${Q} ${RM} ${RM_V} -f jparse_test.log json_api_test ${Q} ${RM} ${RM_V} -f tags ${LOCAL_DIR_TAGS} ${Q} ${RM} ${RM_V} -rf a b ${S} echo @@ -802,6 +802,8 @@ jnum_header.o: ../jparse.h ../jparse.tab.h ../json_parse.h ../json_sem.h \ jnum_test.o: ../jparse.h ../jparse.tab.h ../json_parse.h ../json_sem.h \ ../json_utf8.h ../json_util.h ../util.h ../version.h jnum_chk.h \ jnum_test.c +json_api_test.o: ../jparse.h ../jparse.tab.h ../json_parse.h ../json_sem.h \ + ../json_utf8.h ../json_util.h ../util.h json_api_test.c pr_jparse_test.o: ../jparse.h ../jparse.tab.h ../json_parse.h ../json_sem.h \ ../json_utf8.h ../json_util.h ../util.h ../version.h pr_jparse_test.c \ pr_jparse_test.h diff --git a/version.h b/version.h index 74ba34c..30cd0bc 100644 --- a/version.h +++ b/version.h @@ -55,22 +55,22 @@ * * NOTE: this should match the latest Release string in CHANGES.md */ -#define JPARSE_REPO_VERSION "2.5.11 2026-08-03" /* format: major.minor YYYY-MM-DD */ +#define JPARSE_REPO_VERSION "2.6.0 2026-09-25" /* format: major.minor YYYY-MM-DD */ /* * official jparse version */ -#define JPARSE_TOOL_VERSION "2.0.3 2025-10-02" /* format: major.minor YYYY-MM-DD */ +#define JPARSE_TOOL_VERSION "2.1.0 2026-09-25" /* format: major.minor YYYY-MM-DD */ /* * official JSON parser version */ -#define JPARSE_LIBRARY_VERSION "2.4.4 2026-06-13" /* library version format: major.minor YYYY-MM-DD */ +#define JPARSE_LIBRARY_VERSION "2.5.0 2026-09-25" /* library version format: major.minor YYYY-MM-DD */ /* * official utility functions (util.c) version */ -#define JPARSE_UTILS_VERSION "2.1.8 2026-06-13" /* format: major.minor YYYY-MM-DD */ +#define JPARSE_UTILS_VERSION "2.2.0 2026-06-25" /* format: major.minor YYYY-MM-DD */ #endif /* INCLUDE_JPARSE_VERSION_H */