Core: added json parser

An event-driven JSON parser implementing the grammar of RFC 8259.
The full grammar is recognized: objects, arrays, strings with \uXXXX
escapes (including UTF-16 surrogate pairs), numbers, and the
true/false/null literals.  Tokens are reported to a caller-supplied
handler, with nesting bounded by a configurable maximum depth.

Based on previous work by Maxim Dounin.

Co-authored-by: Dmitry Volyntsev <d.volyntsev@f5.com>
This commit is contained in:
Roman Semenov 2026-08-12 09:40:34 -07:00 • committed by Roman S
parent bc1cb5686b
commit 746609903e
3 changed files with 944 additions and 0 deletions

View file

@ -43,6 +43,7 @@ CORE_DEPS="src/core/nginx.h \
src/core/ngx_crypt.h \
src/core/ngx_data.h \
src/core/ngx_json.h \
src/core/ngx_json_parse.h \
src/core/ngx_json_unescape.h \
src/core/ngx_proxy_protocol.h \
src/core/ngx_syslog.h"
@ -84,6 +85,7 @@ CORE_SRCS="src/core/nginx.c \
src/core/ngx_crypt.c \
src/core/ngx_data.c \
src/core/ngx_json.c \
src/core/ngx_json_parse.c \
src/core/ngx_json_unescape.c \
src/core/ngx_proxy_protocol.c \
src/core/ngx_syslog.c"

831
src/core/ngx_json_parse.c Normal file
View file

@ -0,0 +1,831 @@
/*
* Copyright (C) Maxim Dounin
* Copyright (C) Nginx, Inc.
*/
/*
* JSON parser. RFC 8259 for the JSON grammar.
* Site http://json.org/ may be useful too.
*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_json_parse.h>
#define NGX_JSON_NOT_SKIPPING (ngx_uint_t) -1
#define ngx_json_skipping(d) ((d) != NGX_JSON_NOT_SKIPPING)
#define ngx_json_ws(c) ((c) == ' ' || (c) == '\t' || (c) == CR || (c) == LF)
static ngx_inline ngx_int_t ngx_json_end_number(ngx_json_ctx_t *ctx,
u_char *start, u_char *end, ngx_json_state_e *state);
static ngx_inline ngx_int_t ngx_json_emit(ngx_json_ctx_t *ctx,
ngx_json_event_e event, u_char *data, size_t len);
static ngx_inline ngx_json_state_e ngx_json_after_value(ngx_json_ctx_t *ctx);
static ngx_inline ngx_int_t ngx_json_close(ngx_json_ctx_t *ctx,
ngx_json_event_e event, ngx_json_state_e *state, u_char *p);
static ngx_int_t ngx_json_push_state(ngx_json_ctx_t *ctx,
ngx_json_container_e container);
void
ngx_json_ctx_init(ngx_json_ctx_t *ctx, ngx_pool_t *pool)
{
ctx->state = ngx_json_start;
ctx->pool = pool;
ctx->handler = NULL;
ctx->data = NULL;
ctx->codepoint = 0;
ctx->hex_left = 0;
ctx->depth = 0;
ctx->skip_until_depth = NGX_JSON_NOT_SKIPPING;
ctx->max_depth = NGX_JSON_DEFAULT_MAX_DEPTH;
/* stack is allocated lazily on first push, sized to max_depth */
ctx->stack = NULL;
ctx->in_key = 0;
}
ngx_int_t
ngx_json_parse_ctx(ngx_json_ctx_t *ctx, u_char *data, size_t len)
{
u_char *p, *last, *start;
ngx_int_t rc, hd;
ngx_uint_t skip_depth;
ngx_json_state_e state;
if (len == 0) {
return NGX_DECLINED;
}
if (data == NULL) {
return NGX_ERROR;
}
if (ctx->state == ngx_json_done) {
return NGX_DECLINED;
}
last = data + len;
state = ctx->state;
skip_depth = ctx->skip_until_depth;
start = NULL;
for (p = data; p < last; p++) {
/* skipping suppresses emission only; the machine still validates */
if (ngx_json_skipping(skip_depth) && ctx->depth <= skip_depth
&& (state == ngx_json_object_value_separator
|| state == ngx_json_array_value_separator
|| state == ngx_json_done))
{
skip_depth = NGX_JSON_NOT_SKIPPING;
ctx->skip_until_depth = NGX_JSON_NOT_SKIPPING;
p--;
continue;
}
switch (state) {
case ngx_json_start:
if (ngx_json_ws(*p)) {
break;
}
if (*p == '{') {
rc = ngx_json_push_state(ctx, ngx_json_ctx_object);
if (rc != NGX_OK) {
return rc;
}
if (ngx_json_emit(ctx, NGX_JSON_OBJECT_OPEN, p, 1) != NGX_OK) {
return NGX_ERROR;
}
state = ngx_json_object;
break;
}
if (*p == '[') {
rc = ngx_json_push_state(ctx, ngx_json_ctx_array);
if (rc != NGX_OK) {
return rc;
}
if (ngx_json_emit(ctx, NGX_JSON_ARRAY_OPEN, p, 1) != NGX_OK) {
return NGX_ERROR;
}
state = ngx_json_array;
break;
}
return NGX_DECLINED;
case ngx_json_object:
if (ngx_json_ws(*p)) {
break;
}
if (*p == '}') {
rc = ngx_json_close(ctx, NGX_JSON_OBJECT_CLOSE, &state, p);
if (rc != NGX_OK) {
return rc;
}
break;
}
/* fall through */
case ngx_json_object_next:
if (ngx_json_ws(*p)) {
break;
}
if (*p == '"') {
ctx->in_key = 1;
start = p + 1;
state = ngx_json_string;
break;
}
return NGX_DECLINED;
case ngx_json_object_name_separator:
if (ngx_json_ws(*p)) {
break;
}
if (*p == ':') {
state = ngx_json_value;
break;
}
return NGX_DECLINED;
case ngx_json_object_value_separator:
if (ngx_json_ws(*p)) {
break;
}
if (*p == ',') {
state = ngx_json_object_next;
break;
}
if (*p == '}') {
rc = ngx_json_close(ctx, NGX_JSON_OBJECT_CLOSE, &state, p);
if (rc != NGX_OK) {
return rc;
}
break;
}
return NGX_DECLINED;
case ngx_json_array_value_separator:
if (ngx_json_ws(*p)) {
break;
}
if (*p == ',') {
state = ngx_json_value;
break;
}
if (*p == ']') {
rc = ngx_json_close(ctx, NGX_JSON_ARRAY_CLOSE, &state, p);
if (rc != NGX_OK) {
return rc;
}
break;
}
return NGX_DECLINED;
case ngx_json_array:
if (ngx_json_ws(*p)) {
break;
}
if (*p == ']') {
rc = ngx_json_close(ctx, NGX_JSON_ARRAY_CLOSE, &state, p);
if (rc != NGX_OK) {
return rc;
}
break;
}
/* fall through */
case ngx_json_value:
if (ngx_json_ws(*p)) {
break;
}
start = p;
if (*p == '"') {
ctx->in_key = 0;
start = p + 1;
state = ngx_json_string;
break;
}
if (*p == '{') {
rc = ngx_json_emit(ctx, NGX_JSON_OBJECT_OPEN, p, 1);
if (rc != NGX_OK) {
return rc;
}
rc = ngx_json_push_state(ctx, ngx_json_ctx_object);
if (rc != NGX_OK) {
return rc;
}
state = ngx_json_object;
break;
}
if (*p == '[') {
rc = ngx_json_emit(ctx, NGX_JSON_ARRAY_OPEN, p, 1);
if (rc != NGX_OK) {
return rc;
}
rc = ngx_json_push_state(ctx, ngx_json_ctx_array);
if (rc != NGX_OK) {
return rc;
}
state = ngx_json_array;
break;
}
if (*p == '-') {
state = ngx_json_number_minus;
break;
}
if (*p == '0') {
state = ngx_json_number_int_zero;
break;
}
if (*p >= '1' && *p <= '9') {
state = ngx_json_number_int;
break;
}
if (*p == 't') {
state = ngx_json_true_t;
break;
}
if (*p == 'f') {
state = ngx_json_false_f;
break;
}
if (*p == 'n') {
state = ngx_json_null_n;
break;
}
return NGX_DECLINED;
case ngx_json_string:
if (*p == '"') {
if (ctx->in_key) {
rc = ngx_json_emit(ctx, NGX_JSON_KEY, start, p - start);
if (rc != NGX_OK) {
return rc;
}
ctx->in_key = 0;
state = ngx_json_object_name_separator;
} else {
rc = ngx_json_emit(ctx, NGX_JSON_VALUE_STRING, start,
p - start);
if (rc != NGX_OK) {
return rc;
}
state = ngx_json_after_value(ctx);
}
break;
}
if (*p == '\\') {
state = ngx_json_escaped;
break;
}
if (*p < ' ') {
return NGX_DECLINED;
}
break;
case ngx_json_escaped:
if (*p == '"' || *p == '\\' || *p == '/' || *p == 'b'
|| *p == 'f' || *p == 'n' || *p == 'r' || *p == 't')
{
state = ngx_json_string;
break;
}
if (*p == 'u') {
ctx->codepoint = 0;
ctx->hex_left = 4;
state = ngx_json_escaped_hex;
break;
}
return NGX_DECLINED;
case ngx_json_escaped_hex:
/* accumulate the four hex digits of the first \uXXXX escape */
hd = ngx_json_hex_digit(*p);
if (hd == NGX_ERROR) {
return NGX_DECLINED;
}
ctx->codepoint = (ctx->codepoint << 4) | (uint32_t) hd;
if (--ctx->hex_left > 0) {
break;
}
if (ctx->codepoint >= 0xD800 && ctx->codepoint <= 0xDBFF) {
/* high surrogate - must be followed by \uDC00..\uDFFF */
state = ngx_json_surrogate_start;
break;
}
if (ctx->codepoint >= 0xDC00 && ctx->codepoint <= 0xDFFF) {
/* lone low surrogate */
return NGX_DECLINED;
}
state = ngx_json_string;
break;
case ngx_json_surrogate_start:
if (*p == '\\') {
state = ngx_json_surrogate_u;
break;
}
/* high surrogate not followed by \uXXXX */
return NGX_DECLINED;
case ngx_json_surrogate_u:
if (*p == 'u') {
ctx->codepoint = 0;
ctx->hex_left = 4;
state = ngx_json_surrogate_hex;
break;
}
/* high surrogate followed by non-\u escape */
return NGX_DECLINED;
case ngx_json_surrogate_hex:
/* accumulate the four hex digits of the low \uXXXX escape */
hd = ngx_json_hex_digit(*p);
if (hd == NGX_ERROR) {
return NGX_DECLINED;
}
ctx->codepoint = (ctx->codepoint << 4) | (uint32_t) hd;
if (--ctx->hex_left > 0) {
break;
}
if (ctx->codepoint >= 0xDC00 && ctx->codepoint <= 0xDFFF) {
/* valid surrogate pair */
state = ngx_json_string;
break;
}
/* high surrogate not followed by low surrogate */
return NGX_DECLINED;
case ngx_json_number_minus:
if (*p >= '1' && *p <= '9') {
state = ngx_json_number_int;
break;
}
if (*p == '0') {
state = ngx_json_number_int_zero;
break;
}
return NGX_DECLINED;
case ngx_json_number_int_zero:
if (*p == '.') {
state = ngx_json_number_frac;
break;
}
if (*p == 'e' || *p == 'E') {
/* No sense, but permitted by RFC */
state = ngx_json_number_exp;
break;
}
p--;
rc = ngx_json_end_number(ctx, start, p + 1, &state);
if (rc != NGX_OK) {
return rc;
}
break;
case ngx_json_number_int:
if (*p >= '0' && *p <= '9') {
break;
}
if (*p == '.') {
state = ngx_json_number_frac;
break;
}
if (*p == 'e' || *p == 'E') {
/* No sense, but permitted by RFC */
state = ngx_json_number_exp;
break;
}
p--;
rc = ngx_json_end_number(ctx, start, p + 1, &state);
if (rc!= NGX_OK) {
return rc;
}
break;
case ngx_json_number_frac:
if (*p >= '0' && *p <= '9') {
state = ngx_json_number_frac_digit;
break;
}
return NGX_DECLINED;
case ngx_json_number_frac_digit:
if (*p >= '0' && *p <= '9') {
break;
}
if (*p == 'e' || *p == 'E') {
state = ngx_json_number_exp;
break;
}
p--;
rc = ngx_json_end_number(ctx, start, p + 1, &state);
if (rc != NGX_OK) {
return rc;
}
break;
case ngx_json_number_exp:
if (*p == '-' || *p == '+') {
state = ngx_json_number_exp_plusminus;
break;
}
if (*p >= '0' && *p <= '9') {
state = ngx_json_number_exp_digit;
break;
}
return NGX_DECLINED;
case ngx_json_number_exp_plusminus:
if (*p >= '0' && *p <= '9') {
state = ngx_json_number_exp_digit;
break;
}
return NGX_DECLINED;
case ngx_json_number_exp_digit:
if (*p >= '0' && *p <= '9') {
break;
}
p--;
rc = ngx_json_end_number(ctx, start, p + 1, &state);
if (rc != NGX_OK) {
return rc;
}
break;
case ngx_json_true_t:
if (*p == 'r') {
state = ngx_json_true_tr;
break;
}
return NGX_DECLINED;
case ngx_json_true_tr:
if (*p == 'u') {
state = ngx_json_true_tru;
break;
}
return NGX_DECLINED;
case ngx_json_true_tru:
if (*p == 'e') {
rc = ngx_json_emit(ctx, NGX_JSON_VALUE_BOOL, start,
p + 1 - start);
if (rc != NGX_OK) {
return rc;
}
state = ngx_json_after_value(ctx);
break;
}
return NGX_DECLINED;
case ngx_json_false_f:
if (*p == 'a') {
state = ngx_json_false_fa;
break;
}
return NGX_DECLINED;
case ngx_json_false_fa:
if (*p == 'l') {
state = ngx_json_false_fal;
break;
}
return NGX_DECLINED;
case ngx_json_false_fal:
if (*p == 's') {
state = ngx_json_false_fals;
break;
}
return NGX_DECLINED;
case ngx_json_false_fals:
if (*p == 'e') {
rc = ngx_json_emit(ctx, NGX_JSON_VALUE_BOOL, start,
p + 1 - start);
if (rc != NGX_OK) {
return rc;
}
state = ngx_json_after_value(ctx);
break;
}
return NGX_DECLINED;
case ngx_json_null_n:
if (*p == 'u') {
state = ngx_json_null_nu;
break;
}
return NGX_DECLINED;
case ngx_json_null_nu:
if (*p == 'l') {
state = ngx_json_null_nul;
break;
}
return NGX_DECLINED;
case ngx_json_null_nul:
if (*p == 'l') {
rc = ngx_json_emit(ctx, NGX_JSON_VALUE_NULL, start,
p + 1 - start);
if (rc != NGX_OK) {
return rc;
}
state = ngx_json_after_value(ctx);
break;
}
return NGX_DECLINED;
case ngx_json_done:
if (ngx_json_ws(*p)) {
break;
}
return NGX_DECLINED;
}
skip_depth = ctx->skip_until_depth;
}
ctx->state = state;
if (state == ngx_json_done) {
return NGX_OK;
}
return NGX_DECLINED;
}
ngx_int_t
ngx_json_parse(ngx_pool_t *pool, ngx_str_t *json, ngx_json_handler_pt handler,
void *data)
{
ngx_json_ctx_t jctx;
ngx_json_ctx_init(&jctx, pool);
jctx.data = data;
jctx.handler = handler;
return ngx_json_parse_ctx(&jctx, json->data, json->len);
}
static ngx_inline ngx_int_t
ngx_json_end_number(ngx_json_ctx_t *ctx, u_char *start, u_char *end,
ngx_json_state_e *state)
{
ngx_int_t rc;
rc = ngx_json_emit(ctx, NGX_JSON_VALUE_NUMBER, start, end - start);
if (rc != NGX_OK) {
return rc;
}
*state = ngx_json_after_value(ctx);
return NGX_OK;
}
static ngx_inline ngx_int_t
ngx_json_emit(ngx_json_ctx_t *ctx, ngx_json_event_e event, u_char *data,
size_t len)
{
ngx_int_t rc;
ngx_str_t token;
if (ngx_json_skipping(ctx->skip_until_depth)) {
return NGX_OK;
}
if (ctx->handler == NULL) {
return NGX_OK;
}
token.data = data;
token.len = len;
rc = ctx->handler(ctx, event, &token);
if (rc == NGX_JSON_SKIP) {
/*
* NGX_JSON_SKIP prunes the rest of the current container. It is
* honoured only where it is meaningful:
*
* - OBJECT_OPEN / ARRAY_OPEN: skip the whole container;
* - a scalar VALUE_* that is an array element: skip the rest of
* that array.
*
* A skip requested on a CLOSE, or on an object member value, is
* ignored: object members are navigated by KEY (use a KEY skip),
* and a CLOSE fires after the container is already complete.
*/
switch (event) {
case NGX_JSON_KEY:
case NGX_JSON_OBJECT_OPEN:
case NGX_JSON_ARRAY_OPEN:
ctx->skip_until_depth = ctx->depth;
break;
case NGX_JSON_VALUE_STRING:
case NGX_JSON_VALUE_NUMBER:
case NGX_JSON_VALUE_BOOL:
case NGX_JSON_VALUE_NULL:
if (ctx->depth > 0
&& ctx->stack[ctx->depth - 1] == ngx_json_ctx_array
) {
ctx->skip_until_depth = ctx->depth - 1;
}
break;
default:
break;
}
return NGX_OK;
}
return rc;
}
static ngx_inline ngx_json_state_e
ngx_json_after_value(ngx_json_ctx_t *ctx)
{
if (ctx->depth == 0) {
return ngx_json_done;
}
if (ctx->stack[ctx->depth - 1] == ngx_json_ctx_object) {
return ngx_json_object_value_separator;
}
return ngx_json_array_value_separator;
}
static ngx_inline ngx_int_t
ngx_json_close(ngx_json_ctx_t *ctx, ngx_json_event_e event,
ngx_json_state_e *state, u_char *p)
{
ngx_int_t rc;
ctx->depth--;
rc = ngx_json_emit(ctx, event, p, 1);
if (rc != NGX_OK) {
return rc;
}
*state = ngx_json_after_value(ctx);
return NGX_OK;
}
static ngx_int_t
ngx_json_push_state(ngx_json_ctx_t *ctx, ngx_json_container_e container)
{
ngx_json_container_e *stack;
if (ctx->depth >= ctx->max_depth) {
return NGX_DECLINED;
}
if (ctx->stack == NULL) {
stack = ngx_palloc(ctx->pool,
ctx->max_depth * sizeof(ngx_json_container_e));
if (stack == NULL) {
return NGX_ERROR;
}
ctx->stack = stack;
}
ctx->stack[ctx->depth] = container;
ctx->depth++;
return NGX_OK;
}

111
src/core/ngx_json_parse.h Normal file
View file

@ -0,0 +1,111 @@
/*
* Copyright (C) Maxim Dounin
* Copyright (C) Nginx, Inc.
*/
#ifndef _NGX_JSON_PARSE_H_INCLUDED_
#define _NGX_JSON_PARSE_H_INCLUDED_
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_json_unescape.h>
#define NGX_JSON_SKIP -7
#define NGX_JSON_DEFAULT_MAX_DEPTH 64
typedef enum {
NGX_JSON_OBJECT_OPEN = 0,
NGX_JSON_OBJECT_CLOSE,
NGX_JSON_ARRAY_OPEN,
NGX_JSON_ARRAY_CLOSE,
NGX_JSON_KEY,
NGX_JSON_VALUE_STRING,
NGX_JSON_VALUE_NUMBER,
NGX_JSON_VALUE_BOOL,
NGX_JSON_VALUE_NULL
} ngx_json_event_e;
typedef enum {
ngx_json_start = 0,
ngx_json_done,
ngx_json_object,
ngx_json_object_next,
ngx_json_object_name_separator,
ngx_json_value,
ngx_json_object_value_separator,
ngx_json_array_value_separator,
ngx_json_array,
ngx_json_string,
ngx_json_number_minus,
ngx_json_number_int,
ngx_json_number_int_zero,
ngx_json_number_frac,
ngx_json_number_frac_digit,
ngx_json_number_exp,
ngx_json_number_exp_plusminus,
ngx_json_number_exp_digit,
ngx_json_escaped,
ngx_json_escaped_hex, /* first \uXXXX: reading the four hex digits */
ngx_json_surrogate_start, /* expect '\' beginning the low surrogate */
ngx_json_surrogate_u, /* expect 'u' of the low surrogate */
ngx_json_surrogate_hex, /* low \uXXXX: reading the four hex digits */
ngx_json_true_t,
ngx_json_true_tr,
ngx_json_true_tru,
ngx_json_false_f,
ngx_json_false_fa,
ngx_json_false_fal,
ngx_json_false_fals,
ngx_json_null_n,
ngx_json_null_nu,
ngx_json_null_nul
} ngx_json_state_e;
typedef enum {
ngx_json_ctx_none = 0,
ngx_json_ctx_object,
ngx_json_ctx_array
} ngx_json_container_e;
typedef struct ngx_json_ctx_s ngx_json_ctx_t;
typedef ngx_int_t (*ngx_json_handler_pt)(ngx_json_ctx_t *ctx,
ngx_json_event_e event, ngx_str_t *token);
struct ngx_json_ctx_s {
ngx_json_state_e state;
ngx_json_container_e *stack;
ngx_uint_t depth;
ngx_uint_t max_depth;
uint32_t codepoint;
ngx_uint_t hex_left; /* hex digits still expected */
ngx_uint_t skip_until_depth;
ngx_pool_t *pool;
ngx_json_handler_pt handler;
void *data;
unsigned in_key; /* unsigned in_key:1 */
};
void ngx_json_ctx_init(ngx_json_ctx_t *ctx, ngx_pool_t *pool);
ngx_int_t ngx_json_parse_ctx(ngx_json_ctx_t *ctx, u_char *data, size_t len);
ngx_int_t ngx_json_parse(ngx_pool_t *pool, ngx_str_t *json,
ngx_json_handler_pt handler, void *data);
#endif /* _NGX_JSON_PARSE_H_INCLUDED_ */