Core: added JSON unescape functionality

Decodes JSON string escape sequences in place as defined by
RFC 8259, Section 7: the two-character escapes and \uXXXX,
with non-BMP code points encoded as UTF-16 surrogate pairs
and emitted as UTF-8.
This commit is contained in:
Roman Semenov 2026-08-12 09:39:11 -07:00 • committed by Roman S
parent 3f6f7824d4
commit bc1cb5686b
3 changed files with 304 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_unescape.h \
src/core/ngx_proxy_protocol.h \
src/core/ngx_syslog.h"
@ -83,6 +84,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_unescape.c \
src/core/ngx_proxy_protocol.c \
src/core/ngx_syslog.c"

View file

@ -0,0 +1,277 @@
/*
* Copyright (C) Nginx, Inc.
*/
/*
* JSON string unescape. Decodes escape sequences defined by
* RFC 8259, Section 7, including \uXXXX and UTF-16 surrogate pairs.
*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_json_unescape.h>
static ngx_inline u_char *ngx_json_utf8_encode(u_char *dst,
uint32_t codepoint);
ngx_int_t
ngx_json_unescape_string(ngx_str_t *str)
{
size_t size;
u_char *d, *s, *start, ch;
uint32_t codepoint, high_surrogate;
ngx_int_t n;
ngx_uint_t hex_left;
enum {
sw_usual = 0,
sw_quoted,
sw_hex,
sw_surrogate_start, /* expect '\' of \uDCxx after high surrogate */
sw_surrogate_u /* expect 'u' */
} state;
/*
* RFC 8259, Section 7:
*
* string = quotation-mark *char quotation-mark
*
* char = unescaped /
* escape (
* %x22 / ; " quotation mark U+0022
* %x5C / ; \ reverse solidus U+005C
* %x2F / ; / solidus U+002F
* %x62 / ; b backspace U+0008
* %x66 / ; f form feed U+000C
* %x6E / ; n line feed U+000A
* %x72 / ; r carriage return U+000D
* %x74 / ; t tab U+0009
* %x75 4HEXDIG ) ; uXXXX U+XXXX
*
* Non-BMP code points are encoded as a UTF-16 surrogate pair:
* \uD800..\uDBFF high surrogate
* \uDC00..\uDFFF low surrogate
* The pair decodes to U+10000 + (high - 0xD800) * 0x400
* + (low - 0xDC00).
* The input is an unquoted JSON string body (the bytes between the
* surrounding double quotes); it is decoded in place.
*/
if (str->len == 0) {
return NGX_OK;
}
if (str->data == NULL) {
return NGX_ERROR;
}
start = str->data;
size = str->len;
d = s = start;
state = sw_usual;
codepoint = 0;
high_surrogate = 0;
hex_left = 0;
while (size--) {
ch = *s++;
switch (state) {
case sw_usual:
if (ch == '"') {
return NGX_ERROR;
}
if (ch == '\\') {
state = sw_quoted;
break;
}
if (ch < 0x20) {
/* RFC 8259: control characters must be escaped */
return NGX_ERROR;
}
*d++ = ch;
break;
case sw_quoted:
switch (ch) {
case 'u':
codepoint = 0;
hex_left = 4;
state = sw_hex;
break;
case '"':
case '/':
case '\\':
*d++ = ch;
state = sw_usual;
break;
case 'b':
*d++ = '\b';
state = sw_usual;
break;
case 'f':
*d++ = '\f';
state = sw_usual;
break;
case 'n':
*d++ = '\n';
state = sw_usual;
break;
case 'r':
*d++ = '\r';
state = sw_usual;
break;
case 't':
*d++ = '\t';
state = sw_usual;
break;
default:
return NGX_ERROR;
}
break;
case sw_hex:
n = ngx_json_hex_digit(ch);
if (n == NGX_ERROR) {
return NGX_ERROR;
}
codepoint = (codepoint << 4) | (uint32_t) n;
if (--hex_left > 0) {
break;
}
if (high_surrogate) {
if (codepoint < 0xDC00 || codepoint > 0xDFFF) {
return NGX_ERROR;
}
codepoint = 0x10000
+ ((high_surrogate - 0xD800) << 10)
+ (codepoint - 0xDC00);
d = ngx_json_utf8_encode(d, codepoint);
high_surrogate = 0;
state = sw_usual;
break;
}
if (codepoint >= 0xD800 && codepoint <= 0xDBFF) {
/* high surrogate - wait for the low surrogate */
high_surrogate = codepoint;
state = sw_surrogate_start;
break;
}
if (codepoint >= 0xDC00 && codepoint <= 0xDFFF) {
/* lone low surrogate */
return NGX_ERROR;
}
d = ngx_json_utf8_encode(d, codepoint);
state = sw_usual;
break;
case sw_surrogate_start:
/*
* Expect '\' to begin the low-surrogate escape.
* Lone surrogates are not valid Unicode scalar values
* (RFC 8259, Section 8.2).
*/
if (ch == '\\') {
state = sw_surrogate_u;
break;
}
/* lone high surrogate */
return NGX_ERROR;
case sw_surrogate_u:
if (ch == 'u') {
codepoint = 0;
hex_left = 4;
state = sw_hex;
break;
}
/* lone high surrogate */
return NGX_ERROR;
}
}
if (state != sw_usual) {
/* truncated escape sequence or unpaired high surrogate */
return NGX_ERROR;
}
str->data = start;
str->len = d - start;
return NGX_OK;
}
static ngx_inline u_char *
ngx_json_utf8_encode(u_char *dst, uint32_t codepoint)
{
if (codepoint <= 0x7F) {
*dst++ = (u_char) codepoint;
} else if (codepoint <= 0x7FF) {
*dst++ = (u_char) (0xC0 | (codepoint >> 6));
*dst++ = (u_char) (0x80 | (codepoint & 0x3F));
} else if (codepoint <= 0xFFFF) {
*dst++ = (u_char) (0xE0 | (codepoint >> 12));
*dst++ = (u_char) (0x80 | ((codepoint >> 6) & 0x3F));
*dst++ = (u_char) (0x80 | (codepoint & 0x3F));
} else {
*dst++ = (u_char) (0xF0 | (codepoint >> 18));
*dst++ = (u_char) (0x80 | ((codepoint >> 12) & 0x3F));
*dst++ = (u_char) (0x80 | ((codepoint >> 6) & 0x3F));
*dst++ = (u_char) (0x80 | (codepoint & 0x3F));
}
return dst;
}
ngx_int_t
ngx_json_hex_digit(u_char ch)
{
if (ch >= '0' && ch <= '9') {
return ch - '0';
}
ch = (u_char) (ch | 0x20);
if (ch >= 'a' && ch <= 'f') {
return ch - 'a' + 10;
}
return NGX_ERROR;
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (C) Nginx, Inc.
*/
#ifndef _NGX_JSON_UNESCAPE_H_INCLUDED_
#define _NGX_JSON_UNESCAPE_H_INCLUDED_
#include <ngx_config.h>
#include <ngx_core.h>
/*
* Decode JSON escape sequences (RFC 8259, Section 7) in place. The input
* is an unquoted JSON string body (no surrounding double quotes); on success
* str->len is updated to the decoded length. Returns NGX_OK, or NGX_ERROR
* on a malformed escape or invalid input.
*/
ngx_int_t ngx_json_unescape_string(ngx_str_t *str);
ngx_int_t ngx_json_hex_digit(u_char ch);
#endif /* _NGX_JSON_UNESCAPE_H_INCLUDED_ */