diff options
author | Steven Guerrero <42647963+Soremwar@users.noreply.github.com> | 2020-07-14 13:30:03 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-14 14:30:03 -0400 |
commit | fe8399973a5a1dd8a21cbb6edc88415feb83b2ef (patch) | |
tree | b6c7caa2156bf4694471d913775d2b7bcf7b0d4c /std/node | |
parent | e5724e61189b01bb373b914fb733139a399ac996 (diff) |
feat(std/node): add string_decoder (#6638)
Diffstat (limited to 'std/node')
-rw-r--r-- | std/node/buffer.ts | 4 | ||||
-rw-r--r-- | std/node/events.ts | 1 | ||||
-rw-r--r-- | std/node/module.ts | 29 | ||||
-rw-r--r-- | std/node/string_decoder.ts | 297 | ||||
-rw-r--r-- | std/node/string_decoder_test.ts | 117 |
5 files changed, 434 insertions, 14 deletions
diff --git a/std/node/buffer.ts b/std/node/buffer.ts index dae04a66e..9c8d8784c 100644 --- a/std/node/buffer.ts +++ b/std/node/buffer.ts @@ -3,11 +3,11 @@ import * as base64 from "../encoding/base64.ts"; import { notImplemented, normalizeEncoding } from "./_utils.ts"; const notImplementedEncodings = [ - "utf16le", - "latin1", "ascii", "binary", + "latin1", "ucs2", + "utf16le", ]; function checkEncoding(encoding = "utf8", strict = true): string { diff --git a/std/node/events.ts b/std/node/events.ts index 8d6e90abd..ef547cc37 100644 --- a/std/node/events.ts +++ b/std/node/events.ts @@ -524,3 +524,4 @@ export function on( iterator.return(); } } +export const captureRejectionSymbol = Symbol.for("nodejs.rejection"); diff --git a/std/node/module.ts b/std/node/module.ts index 55c5f2d32..8d13ff366 100644 --- a/std/node/module.ts +++ b/std/node/module.ts @@ -22,13 +22,14 @@ import "./global.ts"; import * as nodeBuffer from "./buffer.ts"; +import * as nodeEvents from "./events.ts"; import * as nodeFS from "./fs.ts"; -import * as nodeUtil from "./util.ts"; +import * as nodeOs from "./os.ts"; import * as nodePath from "./path.ts"; import * as nodeTimers from "./timers.ts"; -import * as nodeOs from "./os.ts"; -import * as nodeEvents from "./events.ts"; import * as nodeQueryString from "./querystring.ts"; +import * as nodeStringDecoder from "./string_decoder.ts"; +import * as nodeUtil from "./util.ts"; import * as path from "../path/mod.ts"; import { assert } from "../_util/assert.ts"; @@ -171,11 +172,13 @@ class Module { return result; } + /* + * Check for node modules paths. + * */ static _resolveLookupPaths( request: string, parent: Module | null ): string[] | null { - // Check for node modules paths. if ( request.charAt(0) !== "." || (request.length > 1 && @@ -195,12 +198,10 @@ class Module { if (!parent || !parent.id || !parent.filename) { // Make require('./path/to/foo') work - normally the path is taken // from realpath(__filename) but with eval there is no filename - const mainPaths = ["."].concat(Module._nodeModulePaths("."), modulePaths); - return mainPaths; + return ["."].concat(Module._nodeModulePaths("."), modulePaths); } - - const parentDir = [path.dirname(parent.filename)]; - return parentDir; + // Returns the parent path of the file + return [path.dirname(parent.filename)]; } static _resolveFilename( @@ -597,16 +598,20 @@ function createNativeModule(id: string, exports: any): Module { } nativeModulePolyfill.set("buffer", createNativeModule("buffer", nodeBuffer)); -nativeModulePolyfill.set("fs", createNativeModule("fs", nodeFS)); nativeModulePolyfill.set("events", createNativeModule("events", nodeEvents)); +nativeModulePolyfill.set("fs", createNativeModule("fs", nodeFS)); nativeModulePolyfill.set("os", createNativeModule("os", nodeOs)); nativeModulePolyfill.set("path", createNativeModule("path", nodePath)); -nativeModulePolyfill.set("timers", createNativeModule("timers", nodeTimers)); -nativeModulePolyfill.set("util", createNativeModule("util", nodeUtil)); nativeModulePolyfill.set( "querystring", createNativeModule("querystring", nodeQueryString) ); +nativeModulePolyfill.set( + "string_decoder", + createNativeModule("string_decoder", nodeStringDecoder) +); +nativeModulePolyfill.set("timers", createNativeModule("timers", nodeTimers)); +nativeModulePolyfill.set("util", createNativeModule("util", nodeUtil)); function loadNativeModule( _filename: string, diff --git a/std/node/string_decoder.ts b/std/node/string_decoder.ts new file mode 100644 index 000000000..7f167ad3c --- /dev/null +++ b/std/node/string_decoder.ts @@ -0,0 +1,297 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { Buffer } from "./buffer.ts"; +import { normalizeEncoding as castEncoding, notImplemented } from "./_utils.ts"; + +enum NotImplemented { + "ascii", + "latin1", + "utf16le", +} + +function normalizeEncoding(enc?: string): string { + const encoding = castEncoding(enc ?? null); + if (encoding && encoding in NotImplemented) notImplemented(encoding); + if (!encoding && typeof enc === "string" && enc.toLowerCase() !== "raw") + throw new Error(`Unknown encoding: ${enc}`); + return String(encoding); +} +/* + * Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a + * continuation byte. If an invalid byte is detected, -2 is returned. + * */ +function utf8CheckByte(byte: number): number { + if (byte <= 0x7f) return 0; + else if (byte >> 5 === 0x06) return 2; + else if (byte >> 4 === 0x0e) return 3; + else if (byte >> 3 === 0x1e) return 4; + return byte >> 6 === 0x02 ? -1 : -2; +} + +/* + * Checks at most 3 bytes at the end of a Buffer in order to detect an + * incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4) + * needed to complete the UTF-8 character (if applicable) are returned. + * */ +function utf8CheckIncomplete( + self: StringDecoderBase, + buf: Buffer, + i: number +): number { + let j = buf.length - 1; + if (j < i) return 0; + let nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) self.lastNeed = nb - 1; + return nb; + } + if (--j < i || nb === -2) return 0; + nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) self.lastNeed = nb - 2; + return nb; + } + if (--j < i || nb === -2) return 0; + nb = utf8CheckByte(buf[j]); + if (nb >= 0) { + if (nb > 0) { + if (nb === 2) nb = 0; + else self.lastNeed = nb - 3; + } + return nb; + } + return 0; +} + +/* + * Validates as many continuation bytes for a multi-byte UTF-8 character as + * needed or are available. If we see a non-continuation byte where we expect + * one, we "replace" the validated continuation bytes we've seen so far with + * a single UTF-8 replacement character ('\ufffd'), to match v8's UTF-8 decoding + * behavior. The continuation byte check is included three times in the case + * where all of the continuation bytes for a character exist in the same buffer. + * It is also done this way as a slight performance increase instead of using a + * loop. + * */ +function utf8CheckExtraBytes( + self: StringDecoderBase, + buf: Buffer +): string | undefined { + if ((buf[0] & 0xc0) !== 0x80) { + self.lastNeed = 0; + return "\ufffd"; + } + if (self.lastNeed > 1 && buf.length > 1) { + if ((buf[1] & 0xc0) !== 0x80) { + self.lastNeed = 1; + return "\ufffd"; + } + if (self.lastNeed > 2 && buf.length > 2) { + if ((buf[2] & 0xc0) !== 0x80) { + self.lastNeed = 2; + return "\ufffd"; + } + } + } +} + +/* + * Attempts to complete a multi-byte UTF-8 character using bytes from a Buffer. + * */ +function utf8FillLastComplete( + this: StringDecoderBase, + buf: Buffer +): string | undefined { + const p = this.lastTotal - this.lastNeed; + const r = utf8CheckExtraBytes(this, buf); + if (r !== undefined) return r; + if (this.lastNeed <= buf.length) { + buf.copy(this.lastChar, p, 0, this.lastNeed); + return this.lastChar.toString(this.encoding, 0, this.lastTotal); + } + buf.copy(this.lastChar, p, 0, buf.length); + this.lastNeed -= buf.length; +} + +/* + * Attempts to complete a partial non-UTF-8 character using bytes from a Buffer + * */ +function utf8FillLastIncomplete( + this: StringDecoderBase, + buf: Buffer +): string | undefined { + if (this.lastNeed <= buf.length) { + buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed); + return this.lastChar.toString(this.encoding, 0, this.lastTotal); + } + buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length); + this.lastNeed -= buf.length; +} + +/* + * Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a + * partial character, the character's bytes are buffered until the required + * number of bytes are available. + * */ +function utf8Text(this: StringDecoderBase, buf: Buffer, i: number): string { + const total = utf8CheckIncomplete(this, buf, i); + if (!this.lastNeed) return buf.toString("utf8", i); + this.lastTotal = total; + const end = buf.length - (total - this.lastNeed); + buf.copy(this.lastChar, 0, end); + return buf.toString("utf8", i, end); +} + +/* + * For UTF-8, a replacement character is added when ending on a partial + * character. + * */ +function utf8End(this: Utf8Decoder, buf?: Buffer): string { + const r = buf && buf.length ? this.write(buf) : ""; + if (this.lastNeed) return r + "\ufffd"; + return r; +} + +function utf8Write(this: Utf8Decoder | Base64Decoder, buf: Buffer): string { + if (buf.length === 0) return ""; + let r; + let i; + if (this.lastNeed) { + r = this.fillLast(buf); + if (r === undefined) return ""; + i = this.lastNeed; + this.lastNeed = 0; + } else { + i = 0; + } + if (i < buf.length) return r ? r + this.text(buf, i) : this.text(buf, i); + return r || ""; +} + +function base64Text(this: StringDecoderBase, buf: Buffer, i: number): string { + const n = (buf.length - i) % 3; + if (n === 0) return buf.toString("base64", i); + this.lastNeed = 3 - n; + this.lastTotal = 3; + if (n === 1) { + this.lastChar[0] = buf[buf.length - 1]; + } else { + this.lastChar[0] = buf[buf.length - 2]; + this.lastChar[1] = buf[buf.length - 1]; + } + return buf.toString("base64", i, buf.length - n); +} + +function base64End(this: Base64Decoder, buf?: Buffer): string { + const r = buf && buf.length ? this.write(buf) : ""; + if (this.lastNeed) + return r + this.lastChar.toString("base64", 0, 3 - this.lastNeed); + return r; +} + +function simpleWrite(this: StringDecoderBase, buf: Buffer): string { + return buf.toString(this.encoding); +} + +function simpleEnd(this: GenericDecoder, buf?: Buffer): string { + return buf && buf.length ? this.write(buf) : ""; +} + +class StringDecoderBase { + public lastChar: Buffer; + public lastNeed = 0; + public lastTotal = 0; + constructor(public encoding: string, nb: number) { + this.lastChar = Buffer.allocUnsafe(nb); + } +} + +class Base64Decoder extends StringDecoderBase { + public end = base64End; + public fillLast = utf8FillLastIncomplete; + public text = base64Text; + public write = utf8Write; + + constructor(encoding?: string) { + super(normalizeEncoding(encoding), 3); + } +} + +class GenericDecoder extends StringDecoderBase { + public end = simpleEnd; + public fillLast = undefined; + public text = utf8Text; + public write = simpleWrite; + + constructor(encoding?: string) { + super(normalizeEncoding(encoding), 4); + } +} + +class Utf8Decoder extends StringDecoderBase { + public end = utf8End; + public fillLast = utf8FillLastComplete; + public text = utf8Text; + public write = utf8Write; + + constructor(encoding?: string) { + super(normalizeEncoding(encoding), 4); + } +} + +/* + * StringDecoder provides an interface for efficiently splitting a series of + * buffers into a series of JS strings without breaking apart multi-byte + * characters. + * */ +export class StringDecoder { + public encoding: string; + public end: (buf?: Buffer) => string; + public fillLast: ((buf: Buffer) => string | undefined) | undefined; + public lastChar: Buffer; + public lastNeed: number; + public lastTotal: number; + public text: (buf: Buffer, n: number) => string; + public write: (buf: Buffer) => string; + + constructor(encoding?: string) { + let decoder; + switch (encoding) { + case "utf8": + decoder = new Utf8Decoder(encoding); + break; + case "base64": + decoder = new Base64Decoder(encoding); + break; + default: + decoder = new GenericDecoder(encoding); + } + this.encoding = decoder.encoding; + this.end = decoder.end; + this.fillLast = decoder.fillLast; + this.lastChar = decoder.lastChar; + this.lastNeed = decoder.lastNeed; + this.lastTotal = decoder.lastTotal; + this.text = decoder.text; + this.write = decoder.write; + } +} diff --git a/std/node/string_decoder_test.ts b/std/node/string_decoder_test.ts new file mode 100644 index 000000000..69bb8402a --- /dev/null +++ b/std/node/string_decoder_test.ts @@ -0,0 +1,117 @@ +import { assertEquals } from "../testing/asserts.ts"; +import Buffer from "./buffer.ts"; +import { StringDecoder } from "./string_decoder.ts"; + +Deno.test({ + name: "String decoder is encoding utf8 correctly", + fn() { + let decoder; + + decoder = new StringDecoder("utf8"); + assertEquals(decoder.write(Buffer.from("E1", "hex")), ""); + assertEquals(decoder.end(), "\ufffd"); + + decoder = new StringDecoder("utf8"); + assertEquals(decoder.write(Buffer.from("E18B", "hex")), ""); + assertEquals(decoder.end(), "\ufffd"); + + decoder = new StringDecoder("utf8"); + assertEquals(decoder.write(Buffer.from("\ufffd")), "\ufffd"); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("utf8"); + assertEquals( + decoder.write(Buffer.from("\ufffd\ufffd\ufffd")), + "\ufffd\ufffd\ufffd" + ); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("utf8"); + assertEquals(decoder.write(Buffer.from("EFBFBDE2", "hex")), "\ufffd"); + assertEquals(decoder.end(), "\ufffd"); + + decoder = new StringDecoder("utf8"); + assertEquals(decoder.write(Buffer.from("F1", "hex")), ""); + assertEquals(decoder.write(Buffer.from("41F2", "hex")), "\ufffdA"); + assertEquals(decoder.end(), "\ufffd"); + + decoder = new StringDecoder("utf8"); + assertEquals(decoder.text(Buffer.from([0x41]), 2), ""); + }, +}); + +Deno.test({ + name: "String decoder is encoding base64 correctly", + fn() { + let decoder; + + decoder = new StringDecoder("base64"); + assertEquals(decoder.write(Buffer.from("E1", "hex")), "4Q=="); + assertEquals(decoder.end(), "4QAA"); + + decoder = new StringDecoder("base64"); + assertEquals(decoder.write(Buffer.from("E18B", "hex")), "4Ys="); + assertEquals(decoder.end(), "4YsA"); + + decoder = new StringDecoder("base64"); + assertEquals(decoder.write(Buffer.from("\ufffd")), "77+9"); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("base64"); + assertEquals( + decoder.write(Buffer.from("\ufffd\ufffd\ufffd")), + "77+977+977+9" + ); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("base64"); + assertEquals(decoder.write(Buffer.from("EFBFBDE2", "hex")), "77+94g=="); + assertEquals(decoder.end(), "4gAA"); + + decoder = new StringDecoder("base64"); + assertEquals(decoder.write(Buffer.from("F1", "hex")), "8Q=="); + assertEquals(decoder.write(Buffer.from("41F2", "hex")), "8UHy"); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("base64"); + assertEquals(decoder.text(Buffer.from([0x41]), 2), "QQ=="); + }, +}); + +Deno.test({ + name: "String decoder is encoding hex correctly", + fn() { + let decoder; + + decoder = new StringDecoder("hex"); + assertEquals(decoder.write(Buffer.from("E1", "hex")), "e1"); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("hex"); + assertEquals(decoder.write(Buffer.from("E18B", "hex")), "e18b"); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("hex"); + assertEquals(decoder.write(Buffer.from("\ufffd")), "efbfbd"); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("hex"); + assertEquals( + decoder.write(Buffer.from("\ufffd\ufffd\ufffd")), + "efbfbdefbfbdefbfbd" + ); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("hex"); + assertEquals(decoder.write(Buffer.from("EFBFBDE2", "hex")), "efbfbde2"); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("hex"); + assertEquals(decoder.write(Buffer.from("F1", "hex")), "f1"); + assertEquals(decoder.write(Buffer.from("41F2", "hex")), "41f2"); + assertEquals(decoder.end(), ""); + + decoder = new StringDecoder("hex"); + assertEquals(decoder.text(Buffer.from([0x41]), 2), ""); + }, +}); |