@ 261105.16
current
'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(), '');
}