From 2b8062c55f812c25958325194f504ec93af9631c Mon Sep 17 00:00:00 2001 From: Tim Lingo Date: Tue, 9 Jun 2026 08:02:46 -0500 Subject: [PATCH] fix(runtime): handle multi-byte UTF-8 in JSON string escaping Validate UTF-8 continuation bytes in jb_emit_escaped; pass valid sequences through and escape orphaned/invalid start bytes as \u00xx. Pre-existing change found uncommitted in the working tree; committed here so it is reviewable rather than lost. Co-Authored-By: Claude Opus 4.8 (1M context) --- lang/el-compiler/runtime/el_runtime.c | 46 +++++++++++++++++++++------ 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/lang/el-compiler/runtime/el_runtime.c b/lang/el-compiler/runtime/el_runtime.c index df83e95..f544846 100644 --- a/lang/el-compiler/runtime/el_runtime.c +++ b/lang/el-compiler/runtime/el_runtime.c @@ -3135,23 +3135,49 @@ static void jb_puts(JsonBuf* b, const char* s) { static void jb_emit_escaped(JsonBuf* b, const char* s) { jb_putc(b, '"'); - for (; *s; s++) { - unsigned char c = (unsigned char)*s; + const unsigned char* p = (const unsigned char*)s; + while (*p) { + unsigned char c = *p; switch (c) { - case '"': jb_puts(b, "\\\""); break; - case '\\': jb_puts(b, "\\\\"); break; - case '\b': jb_puts(b, "\\b"); break; - case '\f': jb_puts(b, "\\f"); break; - case '\n': jb_puts(b, "\\n"); break; - case '\r': jb_puts(b, "\\r"); break; - case '\t': jb_puts(b, "\\t"); break; + case '"': jb_puts(b, "\\\""); p++; break; + case '\\': jb_puts(b, "\\\\"); p++; break; + case '\b': jb_puts(b, "\\b"); p++; break; + case '\f': jb_puts(b, "\\f"); p++; break; + case '\n': jb_puts(b, "\\n"); p++; break; + case '\r': jb_puts(b, "\\r"); p++; break; + case '\t': jb_puts(b, "\\t"); p++; break; default: if (c < 0x20) { char tmp[8]; snprintf(tmp, sizeof(tmp), "\\u%04x", c); jb_puts(b, tmp); - } else { + p++; + } else if (c < 0x80) { jb_putc(b, (char)c); + p++; + } else { + /* Multi-byte UTF-8: validate sequence, pass through if valid, + * escape as \u00xx if the start byte is invalid/orphaned. */ + int seq_len = 0; + if ((c & 0xE0) == 0xC0) seq_len = 2; + else if ((c & 0xF0) == 0xE0) seq_len = 3; + else if ((c & 0xF8) == 0xF0) seq_len = 4; + if (seq_len >= 2) { + int valid = 1; + for (int i = 1; i < seq_len; i++) { + if ((p[i] & 0xC0) != 0x80) { valid = 0; break; } + } + if (valid) { + for (int i = 0; i < seq_len; i++) jb_putc(b, (char)p[i]); + p += seq_len; + break; + } + } + /* Invalid start byte or truncated sequence — escape it */ + char tmp[8]; + snprintf(tmp, sizeof(tmp), "\\u%04x", c); + jb_puts(b, tmp); + p++; } break; }