Skip to content

Commit 8afb2c6

Browse files
committed
util: fix TextEncoder.encodeInto underfilling
Correct UTF-8 length calculation at the two-byte boundary and keep Latin-1 input unsigned while finding the prefix that fits. Handle surrogate pairs atomically in the scalar tail. Use replacement-aware UTF-16 sizing and validate during conversion so valid input avoids a separate validation pass. Fixes: #65994 Signed-off-by: XadillaX <i@2333.moe>
1 parent 565f69f commit 8afb2c6

2 files changed

Lines changed: 101 additions & 50 deletions

File tree

src/encoding_binding.cc

Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,13 @@ constexpr bool isSurrogatePair(uint16_t lead, uint16_t trail) {
8686
return (lead & 0xfc00) == 0xd800 && (trail & 0xfc00) == 0xdc00;
8787
}
8888

89-
constexpr size_t simpleUtfEncodingLength(uint16_t c) {
89+
constexpr size_t simpleUtfEncodingLength(uint8_t c) {
90+
return c < 0x80 ? 1 : 2;
91+
}
92+
93+
constexpr size_t simpleUtfEncodingLength(char16_t c) {
9094
if (c < 0x80) return 1;
91-
if (c < 0x400) return 2;
95+
if (c < 0x800) return 2;
9296
return 3;
9397
}
9498

@@ -114,6 +118,9 @@ constexpr size_t simpleUtfEncodingLength(uint16_t c) {
114118
// multi-byte heavy content.
115119
template <typename Char>
116120
size_t findBestFit(const Char* data, size_t length, size_t bufferSize) {
121+
// TODO: Replace prefix sizing with a bounded simdutf conversion that returns
122+
// both input code units consumed and output bytes written. The current safe
123+
// conversion APIs only return the latter, while encodeInto() requires both.
117124
size_t pos = 0;
118125
size_t utf8Accumulated = 0;
119126
constexpr size_t CHUNK = 257;
@@ -141,14 +148,15 @@ size_t findBestFit(const Char* data, size_t length, size_t bufferSize) {
141148

142149
size_t chunkUtf8Len;
143150
if constexpr (UTF16) {
144-
// TODO(anonrig): Use utf8_length_from_utf16_with_replacement when
145-
// available For now, validate and use utf8_length_from_utf16
146151
size_t newPos = pos + chunkSize;
147152
if (newPos < length && isSurrogatePair(data[newPos - 1], data[newPos]))
148153
chunkSize--;
149-
chunkUtf8Len = simdutf::utf8_length_from_utf16(data + pos, chunkSize);
154+
chunkUtf8Len = simdutf::utf8_length_from_utf16_with_replacement(
155+
data + pos, chunkSize)
156+
.count;
150157
} else {
151-
chunkUtf8Len = simdutf::utf8_length_from_latin1(data + pos, chunkSize);
158+
chunkUtf8Len = simdutf::utf8_length_from_latin1(
159+
reinterpret_cast<const char*>(data + pos), chunkSize);
152160
}
153161

154162
if (utf8Accumulated + chunkUtf8Len > bufferSize) {
@@ -162,19 +170,21 @@ size_t findBestFit(const Char* data, size_t length, size_t bufferSize) {
162170
}
163171

164172
while (pos < length && utf8Accumulated < bufferSize) {
165-
size_t extra = simpleUtfEncodingLength(data[pos]);
166-
if (utf8Accumulated + extra > bufferSize) break;
167-
pos++;
168-
utf8Accumulated += extra;
169-
}
170-
171-
if (UTF16 && pos != 0 && pos != length &&
172-
isSurrogatePair(data[pos - 1], data[pos])) {
173-
if (utf8Accumulated < bufferSize) {
174-
pos++;
173+
size_t codeUnits = 1;
174+
size_t extra;
175+
if constexpr (UTF16) {
176+
if (pos + 1 < length && isSurrogatePair(data[pos], data[pos + 1])) {
177+
codeUnits = 2;
178+
extra = 4;
179+
} else {
180+
extra = simpleUtfEncodingLength(data[pos]);
181+
}
175182
} else {
176-
pos--;
183+
extra = simpleUtfEncodingLength(data[pos]);
177184
}
185+
if (utf8Accumulated + extra > bufferSize) break;
186+
pos += codeUnits;
187+
utf8Accumulated += extra;
178188
}
179189
return pos;
180190
}
@@ -239,9 +249,9 @@ void BindingData::EncodeInto(const FunctionCallbackInfo<Value>& args) {
239249
std::min(static_cast<size_t>(view.length()), dest_length);
240250

241251
if (view.is_one_byte()) {
242-
auto data = reinterpret_cast<const char*>(view.data8());
243-
simdutf::result result =
244-
simdutf::validate_ascii_with_errors(data, length_that_fits);
252+
const uint8_t* data = view.data8();
253+
simdutf::result result = simdutf::validate_ascii_with_errors(
254+
reinterpret_cast<const char*>(data), length_that_fits);
245255
written = read = result.count;
246256
memcpy(write_result, data, read);
247257
write_result += read;
@@ -250,8 +260,9 @@ void BindingData::EncodeInto(const FunctionCallbackInfo<Value>& args) {
250260
dest_length -= read;
251261
if (length_that_fits != 0 && dest_length != 0) {
252262
if (size_t rest = findBestFit(data, length_that_fits, dest_length)) {
253-
DCHECK_LE(simdutf::utf8_length_from_latin1(data, rest), dest_length);
254-
written += simdutf::convert_latin1_to_utf8(data, rest, write_result);
263+
const char* latin1 = reinterpret_cast<const char*>(data);
264+
DCHECK_LE(simdutf::utf8_length_from_latin1(latin1, rest), dest_length);
265+
written += simdutf::convert_latin1_to_utf8(latin1, rest, write_result);
255266
read += rest;
256267
}
257268
}
@@ -266,34 +277,20 @@ void BindingData::EncodeInto(const FunctionCallbackInfo<Value>& args) {
266277
length_that_fits--;
267278
}
268279

269-
// Check if input has unpaired surrogates - if so, convert to well-formed
270-
// first
271-
simdutf::result validation_result =
272-
simdutf::validate_utf16_with_errors(data, length_that_fits);
273-
274-
if (validation_result.error == simdutf::SUCCESS) {
275-
// Valid UTF-16 - use the fast path
276-
read = findBestFit(data, length_that_fits, dest_length);
277-
if (read != 0) {
278-
DCHECK_LE(simdutf::utf8_length_from_utf16(data, read), dest_length);
279-
written = simdutf::convert_utf16_to_utf8(data, read, write_result);
280-
}
281-
} else {
282-
// Invalid UTF-16 with unpaired surrogates - convert to well-formed first
283-
// TODO(anonrig): Use utf8_length_from_utf16_with_replacement when
284-
// available
285-
MaybeStackBuffer<char16_t, MAX_SIZE_FOR_STACK_ALLOC> conversion_buffer(
286-
length_that_fits);
287-
simdutf::to_well_formed_utf16(
288-
data, length_that_fits, conversion_buffer.out());
289-
290-
// Now use findBestFit with the well-formed data
291-
read =
292-
findBestFit(conversion_buffer.out(), length_that_fits, dest_length);
293-
if (read != 0) {
294-
DCHECK_LE(
295-
simdutf::utf8_length_from_utf16(conversion_buffer.out(), read),
296-
dest_length);
280+
read = findBestFit(data, length_that_fits, dest_length);
281+
if (read != 0) {
282+
DCHECK_LE(
283+
simdutf::utf8_length_from_utf16_with_replacement(data, read).count,
284+
dest_length);
285+
simdutf::result conversion_result =
286+
simdutf::convert_utf16_to_utf8_with_errors(data, read, write_result);
287+
if (conversion_result.error == simdutf::SUCCESS) {
288+
written = conversion_result.count;
289+
} else {
290+
// Unpaired surrogates are encoded as U+FFFD.
291+
MaybeStackBuffer<char16_t, MAX_SIZE_FOR_STACK_ALLOC> conversion_buffer(
292+
read);
293+
simdutf::to_well_formed_utf16(data, read, conversion_buffer.out());
297294
written = simdutf::convert_utf16_to_utf8(
298295
conversion_buffer.out(), read, write_result);
299296
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
6+
const encoder = new TextEncoder();
7+
8+
function assertExactFit(character, expectedBytes) {
9+
const input = character.repeat(33);
10+
const destination = new Uint8Array(expectedBytes.length);
11+
12+
assert.deepStrictEqual(
13+
encoder.encodeInto(input, destination),
14+
{ read: character.length, written: expectedBytes.length });
15+
assert.deepStrictEqual(destination, Uint8Array.from(expectedBytes));
16+
17+
assert.deepStrictEqual(
18+
encoder.encodeInto(input, new Uint8Array(expectedBytes.length - 1)),
19+
{ read: 0, written: 0 });
20+
}
21+
22+
// Exercise the optimized path with UTF-8 length boundaries.
23+
assertExactFit('\u03ff', [0xcf, 0xbf]);
24+
assertExactFit('\u0400', [0xd0, 0x80]);
25+
assertExactFit('\u07ff', [0xdf, 0xbf]);
26+
assertExactFit('\u0800', [0xe0, 0xa0, 0x80]);
27+
28+
// One-byte V8 strings must treat Latin-1 code units as unsigned.
29+
assertExactFit('\x80', [0xc2, 0x80]);
30+
assertExactFit('\xff', [0xc3, 0xbf]);
31+
32+
// Appending a two-byte character changes V8's string representation but must
33+
// not affect how much of the preceding text can be encoded.
34+
{
35+
const destination = new Uint8Array(2);
36+
assert.deepStrictEqual(
37+
encoder.encodeInto(`${'\xe9'.repeat(33)}\u263a`, destination),
38+
{ read: 1, written: 2 });
39+
assert.deepStrictEqual(destination, Uint8Array.from([0xc3, 0xa9]));
40+
}
41+
42+
// Keep surrogate handling covered when validation happens during conversion.
43+
assertExactFit('\ud83d\ude00', [0xf0, 0x9f, 0x98, 0x80]);
44+
assertExactFit('\ud800', [0xef, 0xbf, 0xbd]);
45+
46+
// Continue filling the destination after a surrogate pair in the scalar tail.
47+
{
48+
const input = '\u0800\u0800\ud83d\ude00a'.repeat(7);
49+
const destination = new Uint8Array(22);
50+
assert.deepStrictEqual(
51+
encoder.encodeInto(input, destination),
52+
{ read: 10, written: 22 });
53+
assert.deepStrictEqual(destination, encoder.encode(input.slice(0, 10)));
54+
}

0 commit comments

Comments
 (0)