From c856ec53d1297f92aa141fbdaa1e200bc033e910 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Thu, 13 Apr 2023 11:43:32 -0400 Subject: [PATCH] string_decoder: refactor to use private properties --- lib/string_decoder.js | 204 ++++++++++++--------------- test/parallel/test-string-decoder.js | 8 +- 2 files changed, 96 insertions(+), 116 deletions(-) diff --git a/lib/string_decoder.js b/lib/string_decoder.js index 475c6adec0676c..d79ae4d6016504 100644 --- a/lib/string_decoder.js +++ b/lib/string_decoder.js @@ -24,7 +24,6 @@ const { ArrayBufferIsView, ObjectDefineProperties, - Symbol, TypedArrayPrototypeSubarray, } = primordials; @@ -43,129 +42,114 @@ const { const internalUtil = require('internal/util'); const { ERR_INVALID_ARG_TYPE, - ERR_INVALID_THIS, ERR_UNKNOWN_ENCODING, } = require('internal/errors').codes; const isEncoding = Buffer[internalUtil.kIsEncodingSymbol]; -const kNativeDecoder = Symbol('kNativeDecoder'); - -// Do not cache `Buffer.isEncoding` when checking encoding names as some -// modules monkey-patch it to support additional encodings -/** - * Normalize encoding notation - * - * @param {string} enc - * @returns {"utf8" | "utf16le" | "hex" | "ascii" - * | "base64" | "latin1" | "base64url"} - * @throws {TypeError} Throws an error when encoding is invalid - */ -function normalizeEncoding(enc) { - const nenc = internalUtil.normalizeEncoding(enc); - if (nenc === undefined) { - if (Buffer.isEncoding === isEncoding || !Buffer.isEncoding(enc)) - throw new ERR_UNKNOWN_ENCODING(enc); - return enc; - } - return nenc; -} - const encodingsMap = {}; for (let i = 0; i < encodings.length; ++i) encodingsMap[encodings[i]] = i; -/** - * StringDecoder provides an interface for efficiently splitting a series of - * buffers into a series of JS strings without breaking apart multi-byte - * characters. - * - * @param {string} [encoding=utf-8] - */ -function StringDecoder(encoding) { - this.encoding = normalizeEncoding(encoding); - this[kNativeDecoder] = Buffer.alloc(kSize); - this[kNativeDecoder][kEncodingField] = encodingsMap[this.encoding]; -} +class StringDecoder { + #nativeDecoder = Buffer.alloc(kSize); + + /** + * StringDecoder provides an interface for efficiently splitting a series of + * buffers into a series of JS strings without breaking apart multi-byte + * characters. + * + * @param {string} [encoding=utf-8] + */ + constructor(encoding) { + this.encoding = this.#normalizeEncoding(encoding); + this.#nativeDecoder[kEncodingField] = encodingsMap[this.encoding]; + } + + // Do not cache `Buffer.isEncoding` when checking encoding names as some + // modules monkey-patch it to support additional encodings + /** + * Normalize encoding notation + * + * @param {string} enc + * @returns {"utf8" | "utf16le" | "hex" | "ascii" + * | "base64" | "latin1" | "base64url"} + * @throws {TypeError} Throws an error when encoding is invalid + */ + #normalizeEncoding(enc) { + const nenc = internalUtil.normalizeEncoding(enc); + if (nenc === undefined) { + if (Buffer.isEncoding === isEncoding || !Buffer.isEncoding(enc)) + throw new ERR_UNKNOWN_ENCODING(enc); + return enc; + } + return nenc; + } -/** - * Returns a decoded string, omitting any incomplete multi-bytes - * characters at the end of the Buffer, or TypedArray, or DataView - * - * @param {string | Buffer | TypedArray | DataView} buf - * @returns {string} - * @throws {TypeError} Throws when buf is not in one of supported types - */ -StringDecoder.prototype.write = function write(buf) { - if (typeof buf === 'string') - return buf; - if (!ArrayBufferIsView(buf)) - throw new ERR_INVALID_ARG_TYPE('buf', - ['Buffer', 'TypedArray', 'DataView'], - buf); - if (!this[kNativeDecoder]) { - throw new ERR_INVALID_THIS('StringDecoder'); + /** + * Returns a decoded string, omitting any incomplete multi-bytes + * characters at the end of the Buffer, or TypedArray, or DataView + * + * @param {string | Buffer | TypedArray | DataView} buf + * @returns {string} + * @throws {TypeError} Throws when buf is not in one of supported types + */ + write(buf) { + if (typeof buf === 'string') + return buf; + if (!ArrayBufferIsView(buf)) + throw new ERR_INVALID_ARG_TYPE('buf', + ['Buffer', 'TypedArray', 'DataView'], + buf); + return decode(this.#nativeDecoder, buf); } - return decode(this[kNativeDecoder], buf); -}; -/** - * Returns any remaining input stored in the internal buffer as a string. - * After end() is called, the stringDecoder object can be reused for new - * input. - * - * @param {string | Buffer | TypedArray | DataView} [buf] - * @returns {string} - */ -StringDecoder.prototype.end = function end(buf) { - let ret = ''; - if (buf !== undefined) - ret = this.write(buf); - if (this[kNativeDecoder][kBufferedBytes] > 0) - ret += flush(this[kNativeDecoder]); - return ret; -}; + /** + * Returns any remaining input stored in the internal buffer as a string. + * After end() is called, the stringDecoder object can be reused for new + * input. + * + * @param {string | Buffer | TypedArray | DataView} [buf] + * @returns {string} + */ + end(buf) { + let ret = ''; + if (buf !== undefined) + ret = this.write(buf); + if (this.#nativeDecoder[kBufferedBytes] > 0) + ret += flush(this.#nativeDecoder); + return ret; + } + + /* Everything below this line is undocumented legacy stuff. */ + /** + * + * @param {string | Buffer | TypedArray | DataView} buf + * @param {number} offset + * @returns {string} + */ + text(buf, offset) { + this.#nativeDecoder[kMissingBytes] = 0; + this.#nativeDecoder[kBufferedBytes] = 0; + return this.write(buf.slice(offset)); + } + + get lastChar() { + return TypedArrayPrototypeSubarray(this.#nativeDecoder, kIncompleteCharactersStart, kIncompleteCharactersEnd); + } -/* Everything below this line is undocumented legacy stuff. */ -/** - * - * @param {string | Buffer | TypedArray | DataView} buf - * @param {number} offset - * @returns {string} - */ -StringDecoder.prototype.text = function text(buf, offset) { - this[kNativeDecoder][kMissingBytes] = 0; - this[kNativeDecoder][kBufferedBytes] = 0; - return this.write(buf.slice(offset)); -}; + get lastNeed() { + return this.#nativeDecoder[kMissingBytes]; + } + + get lastTotal() { + return this.#nativeDecoder[kBufferedBytes] + this.#nativeDecoder[kMissingBytes]; + } +} ObjectDefineProperties(StringDecoder.prototype, { - lastChar: { - __proto__: null, - configurable: true, - enumerable: true, - get() { - return TypedArrayPrototypeSubarray(this[kNativeDecoder], - kIncompleteCharactersStart, - kIncompleteCharactersEnd); - }, - }, - lastNeed: { - __proto__: null, - configurable: true, - enumerable: true, - get() { - return this[kNativeDecoder][kMissingBytes]; - }, - }, - lastTotal: { - __proto__: null, - configurable: true, - enumerable: true, - get() { - return this[kNativeDecoder][kBufferedBytes] + - this[kNativeDecoder][kMissingBytes]; - }, - }, + lastChar: internalUtil.kEnumerableProperty, + lastNeed: internalUtil.kEnumerableProperty, + lastTotal: internalUtil.kEnumerableProperty, }); exports.StringDecoder = StringDecoder; diff --git a/test/parallel/test-string-decoder.js b/test/parallel/test-string-decoder.js index 02f0a3a718bdec..13c22062aab9be 100644 --- a/test/parallel/test-string-decoder.js +++ b/test/parallel/test-string-decoder.js @@ -29,11 +29,6 @@ const StringDecoder = require('string_decoder').StringDecoder; let decoder = new StringDecoder(); assert.strictEqual(decoder.encoding, 'utf8'); -// Should work without 'new' keyword -const decoder2 = {}; -StringDecoder.call(decoder2); -assert.strictEqual(decoder2.encoding, 'utf8'); - // UTF-8 test('utf-8', Buffer.from('$', 'utf-8'), '$'); test('utf-8', Buffer.from('¢', 'utf-8'), '¢'); @@ -213,7 +208,8 @@ if (common.enoughTestMem) { assert.throws( () => new StringDecoder('utf8').__proto__.write(Buffer.from('abc')), // eslint-disable-line no-proto { - code: 'ERR_INVALID_THIS', + name: 'TypeError', + message: /Cannot read private member/, } );