Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/encoding_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#include <algorithm>
#include <cstdint>
#include <type_traits>

namespace node {
namespace encoding_binding {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<Char>;
size_t extra = simpleUtfEncodingLength(
static_cast<uint16_t>(static_cast<UnsignedChar>(data[pos])));
if (utf8Accumulated + extra > bufferSize) break;
pos++;
utf8Accumulated += extra;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Loading