Skip to content

Commit

Permalink
stream: use private properties for encoding
Browse files Browse the repository at this point in the history
PR-URL: #47218
Reviewed-By: Erick Wendel <erick.workspace@gmail.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Robert Nagy <ronagy@icloud.com>
  • Loading branch information
anonrig authored and nodejs-github-bot committed Apr 13, 2023
1 parent 4e93247 commit 1fa084e
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 73 deletions.
102 changes: 37 additions & 65 deletions lib/internal/webstreams/encoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const {
ObjectDefineProperties,
String,
StringPrototypeCharCodeAt,
Symbol,
Uint8Array,
} = primordials;

Expand All @@ -31,50 +30,37 @@ const {
kEnumerableProperty,
} = require('internal/util');

const kHandle = Symbol('kHandle');
const kTransform = Symbol('kTransform');
const kType = Symbol('kType');
const kPendingHighSurrogate = Symbol('kPendingHighSurrogate');

/**
* @typedef {import('./readablestream').ReadableStream} ReadableStream
* @typedef {import('./writablestream').WritableStream} WritableStream
*/

function isTextEncoderStream(value) {
return typeof value?.[kHandle] === 'object' &&
value?.[kType] === 'TextEncoderStream';
}

function isTextDecoderStream(value) {
return typeof value?.[kHandle] === 'object' &&
value?.[kType] === 'TextDecoderStream';
}

class TextEncoderStream {
#pendingHighSurrogate = null;
#handle;
#transform;

constructor() {
this[kPendingHighSurrogate] = null;
this[kType] = 'TextEncoderStream';
this[kHandle] = new TextEncoder();
this[kTransform] = new TransformStream({
this.#handle = new TextEncoder();
this.#transform = new TransformStream({
transform: (chunk, controller) => {
// https://encoding.spec.whatwg.org/#encode-and-enqueue-a-chunk
chunk = String(chunk);
let finalChunk = '';
for (let i = 0; i < chunk.length; i++) {
const item = chunk[i];
const codeUnit = StringPrototypeCharCodeAt(item, 0);
if (this[kPendingHighSurrogate] !== null) {
const highSurrogate = this[kPendingHighSurrogate];
this[kPendingHighSurrogate] = null;
if (this.#pendingHighSurrogate !== null) {
const highSurrogate = this.#pendingHighSurrogate;
this.#pendingHighSurrogate = null;
if (0xDC00 <= codeUnit && codeUnit <= 0xDFFF) {
finalChunk += highSurrogate + item;
continue;
}
finalChunk += '\uFFFD';
}
if (0xD800 <= codeUnit && codeUnit <= 0xDBFF) {
this[kPendingHighSurrogate] = item;
this.#pendingHighSurrogate = item;
continue;
}
if (0xDC00 <= codeUnit && codeUnit <= 0xDFFF) {
Expand All @@ -84,13 +70,13 @@ class TextEncoderStream {
finalChunk += item;
}
if (finalChunk) {
const value = this[kHandle].encode(finalChunk);
const value = this.#handle.encode(finalChunk);
controller.enqueue(value);
}
},
flush: (controller) => {
// https://encoding.spec.whatwg.org/#encode-and-flush
if (this[kPendingHighSurrogate] !== null) {
if (this.#pendingHighSurrogate !== null) {
controller.enqueue(new Uint8Array([0xEF, 0xBF, 0xBD]));
}
},
Expand All @@ -102,43 +88,40 @@ class TextEncoderStream {
* @type {string}
*/
get encoding() {
if (!isTextEncoderStream(this))
throw new ERR_INVALID_THIS('TextEncoderStream');
return this[kHandle].encoding;
return this.#handle.encoding;
}

/**
* @readonly
* @type {ReadableStream}
*/
get readable() {
if (!isTextEncoderStream(this))
throw new ERR_INVALID_THIS('TextEncoderStream');
return this[kTransform].readable;
return this.#transform.readable;
}

/**
* @readonly
* @type {WritableStream}
*/
get writable() {
if (!isTextEncoderStream(this))
throw new ERR_INVALID_THIS('TextEncoderStream');
return this[kTransform].writable;
return this.#transform.writable;
}

[kInspect](depth, options) {
if (!isTextEncoderStream(this))
if (this == null)
throw new ERR_INVALID_THIS('TextEncoderStream');
return customInspect(depth, options, 'TextEncoderStream', {
encoding: this[kHandle].encoding,
readable: this[kTransform].readable,
writable: this[kTransform].writable,
encoding: this.#handle.encoding,
readable: this.#transform.readable,
writable: this.#transform.writable,
});
}
}

class TextDecoderStream {
#handle;
#transform;

/**
* @param {string} [encoding]
* @param {{
Expand All @@ -147,16 +130,15 @@ class TextDecoderStream {
* }} [options]
*/
constructor(encoding = 'utf-8', options = kEmptyObject) {
this[kType] = 'TextDecoderStream';
this[kHandle] = new TextDecoder(encoding, options);
this[kTransform] = new TransformStream({
this.#handle = new TextDecoder(encoding, options);
this.#transform = new TransformStream({
transform: (chunk, controller) => {
const value = this[kHandle].decode(chunk, { stream: true });
const value = this.#handle.decode(chunk, { stream: true });
if (value)
controller.enqueue(value);
},
flush: (controller) => {
const value = this[kHandle].decode();
const value = this.#handle.decode();
if (value)
controller.enqueue(value);
controller.terminate();
Expand All @@ -169,60 +151,50 @@ class TextDecoderStream {
* @type {string}
*/
get encoding() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kHandle].encoding;
return this.#handle.encoding;
}

/**
* @readonly
* @type {boolean}
*/
get fatal() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kHandle].fatal;
return this.#handle.fatal;
}

/**
* @readonly
* @type {boolean}
*/
get ignoreBOM() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kHandle].ignoreBOM;
return this.#handle.ignoreBOM;
}

/**
* @readonly
* @type {ReadableStream}
*/
get readable() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kTransform].readable;
return this.#transform.readable;
}

/**
* @readonly
* @type {WritableStream}
*/
get writable() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kTransform].writable;
return this.#transform.writable;
}

[kInspect](depth, options) {
if (!isTextDecoderStream(this))
if (this == null)
throw new ERR_INVALID_THIS('TextDecoderStream');
return customInspect(depth, options, 'TextDecoderStream', {
encoding: this[kHandle].encoding,
fatal: this[kHandle].fatal,
ignoreBOM: this[kHandle].ignoreBOM,
readable: this[kTransform].readable,
writable: this[kTransform].writable,
encoding: this.#handle.encoding,
fatal: this.#handle.fatal,
ignoreBOM: this.#handle.ignoreBOM,
readable: this.#transform.readable,
writable: this.#transform.writable,
});
}
}
Expand Down
24 changes: 16 additions & 8 deletions test/parallel/test-whatwg-webstreams-encoding.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,28 @@ const kEuro = Buffer.from([0xe2, 0x82, 0xac]).toString();

assert.throws(
() => Reflect.get(TextDecoderStream.prototype, 'encoding', {}), {
code: 'ERR_INVALID_THIS',
name: 'TypeError',
message: /Cannot read private member/,
});
assert.throws(
() => Reflect.get(TextDecoderStream.prototype, 'fatal', {}), {
code: 'ERR_INVALID_THIS',
name: 'TypeError',
message: /Cannot read private member/,
});
assert.throws(
() => Reflect.get(TextDecoderStream.prototype, 'ignoreBOM', {}), {
code: 'ERR_INVALID_THIS',
name: 'TypeError',
message: /Cannot read private member/,
});
assert.throws(
() => Reflect.get(TextDecoderStream.prototype, 'readable', {}), {
code: 'ERR_INVALID_THIS',
name: 'TypeError',
message: /Cannot read private member/,
});
assert.throws(
() => Reflect.get(TextDecoderStream.prototype, 'writable', {}), {
code: 'ERR_INVALID_THIS',
name: 'TypeError',
message: /Cannot read private member/,
});
}

Expand All @@ -89,14 +94,17 @@ const kEuro = Buffer.from([0xe2, 0x82, 0xac]).toString();

assert.throws(
() => Reflect.get(TextEncoderStream.prototype, 'encoding', {}), {
code: 'ERR_INVALID_THIS',
name: 'TypeError',
message: /Cannot read private member/,
});
assert.throws(
() => Reflect.get(TextEncoderStream.prototype, 'readable', {}), {
code: 'ERR_INVALID_THIS',
name: 'TypeError',
message: /Cannot read private member/,
});
assert.throws(
() => Reflect.get(TextEncoderStream.prototype, 'writable', {}), {
code: 'ERR_INVALID_THIS',
name: 'TypeError',
message: /Cannot read private member/,
});
}

0 comments on commit 1fa084e

Please sign in to comment.