Skip to content

types: keep JSON string encoding valid UTF-8 - #505

Merged
jow- merged 1 commit into
masterfrom
fix/issue-501
Oct 23, 2026
Merged

jow- merged 1 commit into
masterfrom
fix/issue-501

Conversation

@jow-

@jow- jow- commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

The JSON encoder emitted string bytes >= 0x80 verbatim, so encoding a
string that contained invalid UTF-8 (e.g. file contents read via
fs.readfile() passed to printf("%J", …), or to print()/writefile()
as part of an object) produced JSON that is not valid UTF-8, which JSON
parsers reject outright.

Changes

types.c — ucv_to_string_json_encoded():

  • add a strict RFC 3629 validator (utf8_sequence_length(): no overlong
    forms, no surrogates, no codepoints above U+10FFFF, no truncation);
  • valid sequences are passed through unchanged;
  • each invalid byte is replaced by \ufffd, so the encoded output is
    valid UTF-8 no matter which bytes the original string contained.

This is the single choke point for all JSON output (%J,
print()/writefile() of objects/arrays, __tostring__ in JSON
context).

Behaviour

Input %J output
"a" + chr(0xff) + "b" "a\ufffdb"
"caf" + chr(0xc3, 0xa9) "café" (unchanged)
chr(0xc0, 0xaf) (overlong) "\ufffd\ufffd"
chr(0xed, 0xa0, 0x80) (surrogate) "\ufffd\ufffd\ufffd"
"a" + chr(0xe2, 0x82) (truncated) "\ufffd\ufffd"

One replacement character per invalid byte — no guessing at how many
bytes a truncated sequence would have consumed, so no valid bytes get
swallowed.

Tests

  • 03_stdlib/69_json_encode_utf8 — new: encoding behaviour above
  • 03_stdlib/01_chr, 03_stdlib/27_sprintf — expected output updated
    (raw high bytes in expected JSON are now \ufffd)

Full custom test suite: 277/277 pass.

Strings holding bytes that do not form a valid UTF-8 sequence were
emitted verbatim by the JSON encoder, so encoding such a value (e.g.
via printf("%J", ...) or by passing objects to print() or writefile())
produced JSON that is not valid UTF-8, which JSON parsers reject
outright.

Validate the bytes as RFC 3629 while encoding: pass valid sequences
through unchanged and replace each invalid byte by the unicode
replacement character. The encoded output is valid UTF-8 no matter
which bytes the original string contained, at the cost of the
information carried by the invalid bytes only.

The plain I/O paths (fs and io read/write, print) are unaffected and
remain byte-transparent, so binary data round-trips through them
losslessly.

Fixes: #501
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
@jow-
jow- merged commit 60fe09b into master Oct 23, 2026
3 checks passed
@jow-
jow- deleted the fix/issue-501 branch October 23, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant