'use strict'; // The UTF-8 StringDecoder shares its byte->string conversion with // Buffer#toString(): ASCII, Latin-1-representable and general inputs take // different (SIMD) paths depending on content and size, and invalid input // falls back to a replacing decoder. This test pins the decoder's output for // inputs that cross those size thresholds, for chunkings that split multibyte // sequences, and for invalid bytes embedded in otherwise large valid input. require('../common'); const assert = require('assert'); const { StringDecoder } = require('string_decoder'); function decodeInChunks(buf, chunkSize) { const decoder = new StringDecoder('utf8'); let out = ''; for (let i = 0; i < buf.length; i += chunkSize) { out += decoder.write(buf.subarray(i, i + chunkSize)); } return out + decoder.end(); } function check(str, label) { const buf = Buffer.from(str, 'utf8'); // Sanity: the expectation itself round-trips. assert.strictEqual(buf.toString('utf8'), str, `${label}: toString`); for (const chunkSize of [1, 2, 3, 4, 5, 7, 31, 32, 33, 255, 256, 257, 4095, 4096, 65536, buf.length]) { if (chunkSize > buf.length) continue; // Keep the test fast: byte-sized chunks only for the smaller inputs. if (buf.length > 100_000 && chunkSize < 4095) continue; assert.strictEqual(decodeInChunks(buf, chunkSize), str, `${label}: chunkSize=${chunkSize}`); } } const sizes = [31, 32, 33, 255, 256, 257, 4096, 70000, (1 << 20) + 5]; for (const size of sizes) { check('a'.repeat(size), `ascii ${size}`); // Latin-1 range only (one-byte string in V8, two bytes each in UTF-8). check('é'.repeat(size), `latin1 ${size}`); // ASCII with a single Latin-1 character at the end / start. check('a'.repeat(size - 1) + 'ÿ', `ascii+latin1 tail ${size}`); check('Ä' + 'a'.repeat(size - 1), `latin1 head+ascii ${size}`); // BMP beyond Latin-1 (three-byte sequences). check('日'.repeat(size), `cjk ${size}`); // Mixed, including astral plane characters (surrogate pairs, 4 bytes). check(('abé日\u{1F600}').repeat(Math.ceil(size / 6)), `mixed ${size}`); } // Invalid bytes inside otherwise valid input of every size class must still be // replaced with U+FFFD exactly as before, regardless of chunking. for (const size of [8, 40, 300, 5000, (1 << 20) + 5]) { const valid = Buffer.from('a'.repeat(size)); for (const bad of [[0xff], [0xc0, 0xaf], [0xe2, 0x28, 0xa1], [0xed, 0xa0, 0x80] /* encoded surrogate */, [0xf0, 0x9f, 0x98] /* truncated 4-byte */]) { const buf = Buffer.concat([valid, Buffer.from(bad), valid]); const expected = buf.toString('utf8'); assert.ok(expected.includes('�'), `size=${size} bad=${bad}`); for (const chunkSize of [1, 3, 64, size, size + 1, buf.length]) { if (buf.length > 100_000 && chunkSize < size) continue; assert.strictEqual(decodeInChunks(buf, chunkSize), expected, `invalid ${bad} in ${size}, chunkSize=${chunkSize}`); } } } // A lone continuation / lead byte split across the size classes at the very // end is buffered by the decoder and flushed as U+FFFD by end(). { const decoder = new StringDecoder('utf8'); const big = Buffer.concat([Buffer.from('a'.repeat(300)), Buffer.from([0xe2, 0x82])]); assert.strictEqual(decoder.write(big), 'a'.repeat(300)); assert.strictEqual(decoder.end(), '�'); } { const decoder = new StringDecoder('utf8'); const big = Buffer.concat([Buffer.from('é'.repeat(300)), Buffer.from([0xe2, 0x82])]); assert.strictEqual(decoder.write(big), 'é'.repeat(300)); assert.strictEqual(decoder.write(Buffer.from([0xac])), '€'); assert.strictEqual(decoder.end(), ''); }