Skip to content

Commit

Permalink
string_decoder: refactor to use private properties
Browse files Browse the repository at this point in the history
  • Loading branch information
anonrig committed Apr 13, 2023
1 parent 1323992 commit c856ec5
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 116 deletions.
204 changes: 94 additions & 110 deletions lib/string_decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
const {
ArrayBufferIsView,
ObjectDefineProperties,
Symbol,
TypedArrayPrototypeSubarray,
} = primordials;

Expand All @@ -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;
8 changes: 2 additions & 6 deletions test/parallel/test-string-decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'), '¢');
Expand Down Expand Up @@ -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/,
}
);

Expand Down

0 comments on commit c856ec5

Please sign in to comment.