GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — string_decoder: decode UTF-8 via StringBytes::Encode · nodejs/node@f33dba7

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • src
    • string_decoder.cc
  • test/parallel
    • test-string-decoder-utf8-large.js
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/string_decoder.cc‎

Copy file name to clipboardExpand all lines: src/string_decoder.cc
+13-15Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,23 +28,21 @@ MaybeLocal<String> MakeString(Isolate* isolate,
2828
const char* data,
2929
size_t length,
3030
enum encoding encoding) {
31-
MaybeLocal<Value> ret;
32-
if (encoding == UTF8) {
33-
MaybeLocal<String> utf8_string;
34-
if (length <= static_cast<size_t>(v8::String::kMaxLength)) {
35-
utf8_string = String::NewFromUtf8(
36-
isolate, data, v8::NewStringType::kNormal, length);
37-
}
38-
if (utf8_string.IsEmpty()) {
39-
isolate->ThrowException(node::ERR_STRING_TOO_LONG(isolate));
40-
return MaybeLocal<String>();
41-
} else {
42-
return utf8_string;
43-
}
44-
} else {
45-
ret = StringBytes::Encode(isolate, data, length, encoding);
31+
// StringBytes::Encode() would report an over-long UTF-8 input as
32+
// ERR_BUFFER_TOO_LARGE (or clamp it); keep reporting it the way this
33+
// decoder always has.
34+
if (encoding == UTF8 && length > static_cast<size_t>(v8::String::kMaxLength))
35+
[[unlikely]] {
36+
isolate->ThrowException(node::ERR_STRING_TOO_LONG(isolate));
37+
return MaybeLocal<String>();
4638
}
4739

40+
// For UTF-8 this takes the simdutf-backed ASCII / Latin-1 / UTF-16 paths and
41+
// only falls back to v8::String::NewFromUtf8() (the previous unconditional
42+
// path here) for input containing invalid sequences, so U+FFFD replacement
43+
// is unchanged.
44+
MaybeLocal<Value> ret = StringBytes::Encode(isolate, data, length, encoding);
45+
4846
if (ret.IsEmpty()) {
4947
return {};
5048
}
Collapse file

‎test/parallel/test-string-decoder-utf8-large.js‎

Copy file name to clipboard
+81Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use strict';
2+
// The UTF-8 StringDecoder shares its byte->string conversion with
3+
// Buffer#toString(): ASCII, Latin-1-representable and general inputs take
4+
// different (SIMD) paths depending on content and size, and invalid input
5+
// falls back to a replacing decoder. This test pins the decoder's output for
6+
// inputs that cross those size thresholds, for chunkings that split multibyte
7+
// sequences, and for invalid bytes embedded in otherwise large valid input.
8+
require('../common');
9+
const assert = require('assert');
10+
const { StringDecoder } = require('string_decoder');
11+
12+
function decodeInChunks(buf, chunkSize) {
13+
const decoder = new StringDecoder('utf8');
14+
let out = '';
15+
for (let i = 0; i < buf.length; i += chunkSize) {
16+
out += decoder.write(buf.subarray(i, i + chunkSize));
17+
}
18+
return out + decoder.end();
19+
}
20+
21+
function check(str, label) {
22+
const buf = Buffer.from(str, 'utf8');
23+
// Sanity: the expectation itself round-trips.
24+
assert.strictEqual(buf.toString('utf8'), str, `${label}: toString`);
25+
for (const chunkSize of [1, 2, 3, 4, 5, 7, 31, 32, 33, 255, 256, 257,
26+
4095, 4096, 65536, buf.length]) {
27+
if (chunkSize > buf.length) continue;
28+
// Keep the test fast: byte-sized chunks only for the smaller inputs.
29+
if (buf.length > 100_000 && chunkSize < 4095) continue;
30+
assert.strictEqual(decodeInChunks(buf, chunkSize), str,
31+
`${label}: chunkSize=${chunkSize}`);
32+
}
33+
}
34+
35+
const sizes = [31, 32, 33, 255, 256, 257, 4096, 70000, (1 << 20) + 5];
36+
for (const size of sizes) {
37+
check('a'.repeat(size), `ascii ${size}`);
38+
// Latin-1 range only (one-byte string in V8, two bytes each in UTF-8).
39+
check('é'.repeat(size), `latin1 ${size}`);
40+
// ASCII with a single Latin-1 character at the end / start.
41+
check('a'.repeat(size - 1) + 'ÿ', `ascii+latin1 tail ${size}`);
42+
check('Ä' + 'a'.repeat(size - 1), `latin1 head+ascii ${size}`);
43+
// BMP beyond Latin-1 (three-byte sequences).
44+
check('日'.repeat(size), `cjk ${size}`);
45+
// Mixed, including astral plane characters (surrogate pairs, 4 bytes).
46+
check(('abé日\u{1F600}').repeat(Math.ceil(size / 6)), `mixed ${size}`);
47+
}
48+
49+
// Invalid bytes inside otherwise valid input of every size class must still be
50+
// replaced with U+FFFD exactly as before, regardless of chunking.
51+
for (const size of [8, 40, 300, 5000, (1 << 20) + 5]) {
52+
const valid = Buffer.from('a'.repeat(size));
53+
for (const bad of [[0xff], [0xc0, 0xaf], [0xe2, 0x28, 0xa1],
54+
[0xed, 0xa0, 0x80] /* encoded surrogate */,
55+
[0xf0, 0x9f, 0x98] /* truncated 4-byte */]) {
56+
const buf = Buffer.concat([valid, Buffer.from(bad), valid]);
57+
const expected = buf.toString('utf8');
58+
assert.ok(expected.includes('�'), `size=${size} bad=${bad}`);
59+
for (const chunkSize of [1, 3, 64, size, size + 1, buf.length]) {
60+
if (buf.length > 100_000 && chunkSize < size) continue;
61+
assert.strictEqual(decodeInChunks(buf, chunkSize), expected,
62+
`invalid ${bad} in ${size}, chunkSize=${chunkSize}`);
63+
}
64+
}
65+
}
66+
67+
// A lone continuation / lead byte split across the size classes at the very
68+
// end is buffered by the decoder and flushed as U+FFFD by end().
69+
{
70+
const decoder = new StringDecoder('utf8');
71+
const big = Buffer.concat([Buffer.from('a'.repeat(300)), Buffer.from([0xe2, 0x82])]);
72+
assert.strictEqual(decoder.write(big), 'a'.repeat(300));
73+
assert.strictEqual(decoder.end(), '�');
74+
}
75+
{
76+
const decoder = new StringDecoder('utf8');
77+
const big = Buffer.concat([Buffer.from('é'.repeat(300)), Buffer.from([0xe2, 0x82])]);
78+
assert.strictEqual(decoder.write(big), 'é'.repeat(300));
79+
assert.strictEqual(decoder.write(Buffer.from([0xac])), '€');
80+
assert.strictEqual(decoder.end(), '');
81+
}

0 commit comments

Comments
0 (0)
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.