]> git.kaiwu.me - nginx.git/commitdiff
Core: added JSON unescape functionality
authorRoman Semenov <r.semenov@f5.com>
Wed, 12 Aug 2026 16:39:11 +0000 (09:39 -0700)
committerRoman S <141866642+roman-f5@users.noreply.github.com>
Mon, 24 Aug 2026 18:08:31 +0000 (11:08 -0700)
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.

auto/sources
src/core/ngx_json_unescape.c [new file with mode: 0644]
src/core/ngx_json_unescape.h [new file with mode: 0644]

index 52a59d0896b661ec3c28be526d82207eec83f47b..022e0087059e286c81da9217290623b7e6148f93 100644 (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"
 
diff --git a/src/core/ngx_json_unescape.c b/src/core/ngx_json_unescape.c
new file mode 100644 (file)
index 0000000..a828780
--- /dev/null
@@ -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;
+}
diff --git a/src/core/ngx_json_unescape.h b/src/core/ngx_json_unescape.h
new file mode 100644 (file)
index 0000000..8fb0f54
--- /dev/null
@@ -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_ */