From 5ad8645949a4c3496f22a80b03a0617d625629a7 Mon Sep 17 00:00:00 2001 From: Shani Singh Date: Thu, 6 Aug 2026 08:07:21 +0530 Subject: [PATCH] src: fix UTF-8 two byte threshold in encodeInto `simpleUtfEncodingLength()` returned 3 for every code point at or above U+0400, but UTF-8 encodes U+0080 through U+07FF in two bytes. The comment on `findBestFit()` a few lines below already states the correct rule. The helper is the boundary refinement step of `findBestFit()`, which decides how many code units of the input fit in the destination of `TextEncoder.prototype.encodeInto()`. Over-costing Cyrillic, Hebrew, Arabic and the other two byte blocks made it stop early, so `encodeInto()` left the destination short and under-reported `read` and `written`. The helper only ever over-estimates, so the destination is never overrun. Signed-off-by: Shani Singh --- src/encoding_binding.cc | 2 +- .../test-text-encoder-encode-into-two-byte.js | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-text-encoder-encode-into-two-byte.js diff --git a/src/encoding_binding.cc b/src/encoding_binding.cc index 8a445088b54a..bb2f803d7d37 100644 --- a/src/encoding_binding.cc +++ b/src/encoding_binding.cc @@ -88,7 +88,7 @@ constexpr bool isSurrogatePair(uint16_t lead, uint16_t trail) { constexpr size_t simpleUtfEncodingLength(uint16_t c) { if (c < 0x80) return 1; - if (c < 0x400) return 2; + if (c < 0x800) return 2; return 3; } diff --git a/test/parallel/test-text-encoder-encode-into-two-byte.js b/test/parallel/test-text-encoder-encode-into-two-byte.js new file mode 100644 index 000000000000..ded9fdd6d1b2 --- /dev/null +++ b/test/parallel/test-text-encoder-encode-into-two-byte.js @@ -0,0 +1,48 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); + +// `encodeInto()` must write as many code points as fit in the destination. +// Code points in U+0080..U+07FF occupy two UTF-8 bytes, so a destination of +// N bytes must take floor(N / 2) of them. The internal helper that decides +// where to stop near the buffer boundary treated the two byte range as +// U+0080..U+03FF, so anything from U+0400 upwards was costed as three bytes +// and the destination was left short. + +const encoder = new TextEncoder(); + +// One representative per affected block: Cyrillic, Hebrew, and the last +// code point that still encodes to two bytes. +for (const char of ['Ѐ', 'б', 'א', '߿']) { + assert.strictEqual(Buffer.byteLength(char, 'utf8'), 2); + + const input = char.repeat(64); + for (const destLength of [2, 3, 4, 10, 40, 100]) { + const dest = new Uint8Array(destLength); + const { read, written } = encoder.encodeInto(input, dest); + + const expectedRead = Math.floor(destLength / 2); + const message = + `U+${char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')} ` + + `into ${destLength} bytes`; + + assert.strictEqual(read, expectedRead, `read for ${message}`); + assert.strictEqual(written, expectedRead * 2, `written for ${message}`); + + // The bytes that were written must be the correct encoding. + assert.deepStrictEqual( + dest.subarray(0, written), + new Uint8Array(Buffer.from(char.repeat(expectedRead), 'utf8')), + `bytes for ${message}`, + ); + } +} + +// A three byte code point still costs three bytes. +{ + const dest = new Uint8Array(4); + const { read, written } = encoder.encodeInto('ࠀࠀ', dest); + assert.strictEqual(read, 1); + assert.strictEqual(written, 3); +}