node/lib/internal/webstreams/encoding.js
devstone 3111ed7011
stream: handle undefined chunks correctly in decode stream
Align TextDecoderStream behavior with WPT requirements by treating
undefined chunks as errors. This change ensures that TextDecoderStream
properly handles unexpected chunk types and throws an error when
receiving undefined input.

This update addresses the failing WPT for decode stream error handling.

PR-URL: https://github.com/nodejs/node/pull/55153
Reviewed-By: Mattias Buelens <mattias@buelens.com>
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
2024-09-30 17:54:43 +00:00

224 lines
5.0 KiB
JavaScript

'use strict';
const {
ObjectDefineProperties,
String,
StringPrototypeCharCodeAt,
Uint8Array,
} = primordials;
const {
TextDecoder,
TextEncoder,
} = require('internal/encoding');
const {
TransformStream,
} = require('internal/webstreams/transformstream');
const { customInspect } = require('internal/webstreams/util');
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_THIS,
},
} = require('internal/errors');
const {
customInspectSymbol: kInspect,
kEmptyObject,
kEnumerableProperty,
} = require('internal/util');
/**
* @typedef {import('./readablestream').ReadableStream} ReadableStream
* @typedef {import('./writablestream').WritableStream} WritableStream
*/
class TextEncoderStream {
#pendingHighSurrogate = null;
#handle;
#transform;
constructor() {
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.#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.#pendingHighSurrogate = item;
continue;
}
if (0xDC00 <= codeUnit && codeUnit <= 0xDFFF) {
finalChunk += '\uFFFD';
continue;
}
finalChunk += item;
}
if (finalChunk) {
const value = this.#handle.encode(finalChunk);
controller.enqueue(value);
}
},
flush: (controller) => {
// https://encoding.spec.whatwg.org/#encode-and-flush
if (this.#pendingHighSurrogate !== null) {
controller.enqueue(new Uint8Array([0xEF, 0xBF, 0xBD]));
}
},
});
}
/**
* @readonly
* @type {string}
*/
get encoding() {
return this.#handle.encoding;
}
/**
* @readonly
* @type {ReadableStream}
*/
get readable() {
return this.#transform.readable;
}
/**
* @readonly
* @type {WritableStream}
*/
get writable() {
return this.#transform.writable;
}
[kInspect](depth, options) {
if (this == null)
throw new ERR_INVALID_THIS('TextEncoderStream');
return customInspect(depth, options, 'TextEncoderStream', {
encoding: this.#handle.encoding,
readable: this.#transform.readable,
writable: this.#transform.writable,
});
}
}
class TextDecoderStream {
#handle;
#transform;
/**
* @param {string} [encoding]
* @param {{
* fatal? : boolean,
* ignoreBOM? : boolean,
* }} [options]
*/
constructor(encoding = 'utf-8', options = kEmptyObject) {
this.#handle = new TextDecoder(encoding, options);
this.#transform = new TransformStream({
transform: (chunk, controller) => {
if (chunk === undefined) {
throw new ERR_INVALID_ARG_TYPE('chunk', 'string', chunk);
}
const value = this.#handle.decode(chunk, { stream: true });
if (value)
controller.enqueue(value);
},
flush: (controller) => {
const value = this.#handle.decode();
if (value)
controller.enqueue(value);
controller.terminate();
},
});
}
/**
* @readonly
* @type {string}
*/
get encoding() {
return this.#handle.encoding;
}
/**
* @readonly
* @type {boolean}
*/
get fatal() {
return this.#handle.fatal;
}
/**
* @readonly
* @type {boolean}
*/
get ignoreBOM() {
return this.#handle.ignoreBOM;
}
/**
* @readonly
* @type {ReadableStream}
*/
get readable() {
return this.#transform.readable;
}
/**
* @readonly
* @type {WritableStream}
*/
get writable() {
return this.#transform.writable;
}
[kInspect](depth, options) {
if (this == null)
throw new ERR_INVALID_THIS('TextDecoderStream');
return customInspect(depth, options, 'TextDecoderStream', {
encoding: this.#handle.encoding,
fatal: this.#handle.fatal,
ignoreBOM: this.#handle.ignoreBOM,
readable: this.#transform.readable,
writable: this.#transform.writable,
});
}
}
ObjectDefineProperties(TextEncoderStream.prototype, {
encoding: kEnumerableProperty,
readable: kEnumerableProperty,
writable: kEnumerableProperty,
});
ObjectDefineProperties(TextDecoderStream.prototype, {
encoding: kEnumerableProperty,
fatal: kEnumerableProperty,
ignoreBOM: kEnumerableProperty,
readable: kEnumerableProperty,
writable: kEnumerableProperty,
});
module.exports = {
TextEncoderStream,
TextDecoderStream,
};