diff --git a/src/encoding_binding.cc b/src/encoding_binding.cc index 8a445088b54a..dadaed04e2fe 100644 --- a/src/encoding_binding.cc +++ b/src/encoding_binding.cc @@ -10,6 +10,7 @@ #include #include +#include namespace node { namespace encoding_binding { @@ -88,7 +89,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; } @@ -162,7 +163,13 @@ size_t findBestFit(const Char* data, size_t length, size_t bufferSize) { } while (pos < length && utf8Accumulated < bufferSize) { - size_t extra = simpleUtfEncodingLength(data[pos]); + // `char` is signed on some platforms/ABIs, so widening a byte >= 0x80 + // straight to uint16_t would sign-extend it into a bogus code point. + // Go through the Char type's unsigned counterpart first (a no-op for + // char16_t, which is unsigned already) to get the right code unit. + using UnsignedChar = std::make_unsigned_t; + size_t extra = simpleUtfEncodingLength( + static_cast(static_cast(data[pos]))); if (utf8Accumulated + extra > bufferSize) break; pos++; utf8Accumulated += extra; diff --git a/test/parallel/test-whatwg-encoding-custom-textencoder-encodeinto.js b/test/parallel/test-whatwg-encoding-custom-textencoder-encodeinto.js new file mode 100644 index 000000000000..5633d6310910 --- /dev/null +++ b/test/parallel/test-whatwg-encoding-custom-textencoder-encodeinto.js @@ -0,0 +1,39 @@ +'use strict'; + +// This tests that TextEncoder.encodeInto() does not underestimate how many +// bytes a code point needs when computing how much of the source string fits +// into the destination. + +require('../common'); +const assert = require('assert'); + +// Long enough to bypass the small-string fast path (kSmallStringThreshold = 32 +// in src/encoding_binding.cc) and exercise the chunked encoding logic. +const encoder = new TextEncoder(); + +{ + // Code points in [0x80, 0x800) are 2 bytes in UTF-8; treating them as 3 + // bytes causes encodeInto() to reject input that would actually fit. + const text = 'Ѐ'.repeat(33); + const result = encoder.encodeInto(text, new Uint8Array(2)); + assert.strictEqual(result.read, 1); + assert.strictEqual(result.written, 2); +} + +{ + // A one-byte (Latin1) source string takes a different internal path than + // a two-byte (UTF-16) one. Bytes >= 0x80 must be treated as unsigned there + // too, or they get sign-extended into a bogus, oversized code point. + const text = 'é'.repeat(33); + const result = encoder.encodeInto(text, new Uint8Array(2)); + assert.strictEqual(result.read, 1); + assert.strictEqual(result.written, 2); + + // Appending a two-byte character forces the whole string to be stored as + // UTF-16 internally, which must not change how the Latin1-only prefix + // encodes. + const withTrailingChar = encoder.encodeInto( + text + '☺', new Uint8Array(2)); + assert.strictEqual(withTrailingChar.read, result.read); + assert.strictEqual(withTrailingChar.written, result.written); +}