1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-26 16:09:27 -05:00
denoland-deno/op_crates/web/08_text_encoding.js
Bartek Iwańczuk 41215eb29c
Op crate for Web APIs (#6906)
Co-authored-by: Ryan Dahl <ry@tinyclouds.org>
2020-08-07 16:55:02 +02:00

838 lines
21 KiB
JavaScript

// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
// The following code is based off of text-encoding at:
// https://github.com/inexorabletash/text-encoding
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// 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 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.
((window) => {
const core = Deno.core;
const CONTINUE = null;
const END_OF_STREAM = -1;
const FINISHED = -1;
function decoderError(fatal) {
if (fatal) {
throw new TypeError("Decoder error.");
}
return 0xfffd; // default code point
}
function inRange(a, min, max) {
return min <= a && a <= max;
}
function isASCIIByte(a) {
return inRange(a, 0x00, 0x7f);
}
function stringToCodePoints(input) {
const u = [];
for (const c of input) {
u.push(c.codePointAt(0));
}
return u;
}
class UTF8Encoder {
handler(codePoint) {
if (codePoint === END_OF_STREAM) {
return "finished";
}
if (inRange(codePoint, 0x00, 0x7f)) {
return [codePoint];
}
let count;
let offset;
if (inRange(codePoint, 0x0080, 0x07ff)) {
count = 1;
offset = 0xc0;
} else if (inRange(codePoint, 0x0800, 0xffff)) {
count = 2;
offset = 0xe0;
} else if (inRange(codePoint, 0x10000, 0x10ffff)) {
count = 3;
offset = 0xf0;
} else {
throw TypeError(
`Code point out of range: \\x${codePoint.toString(16)}`,
);
}
const bytes = [(codePoint >> (6 * count)) + offset];
while (count > 0) {
const temp = codePoint >> (6 * (count - 1));
bytes.push(0x80 | (temp & 0x3f));
count--;
}
return bytes;
}
}
function atob(s) {
s = String(s);
s = s.replace(/[\t\n\f\r ]/g, "");
if (s.length % 4 === 0) {
s = s.replace(/==?$/, "");
}
const rem = s.length % 4;
if (rem === 1 || /[^+/0-9A-Za-z]/.test(s)) {
throw new DOMException(
"The string to be decoded is not correctly encoded",
"DataDecodeError",
);
}
// base64-js requires length exactly times of 4
if (rem > 0) {
s = s.padEnd(s.length + (4 - rem), "=");
}
const byteArray = base64.toByteArray(s);
let result = "";
for (let i = 0; i < byteArray.length; i++) {
result += String.fromCharCode(byteArray[i]);
}
return result;
}
function btoa(s) {
const byteArray = [];
for (let i = 0; i < s.length; i++) {
const charCode = s[i].charCodeAt(0);
if (charCode > 0xff) {
throw new TypeError(
"The string to be encoded contains characters " +
"outside of the Latin1 range.",
);
}
byteArray.push(charCode);
}
const result = base64.fromByteArray(Uint8Array.from(byteArray));
return result;
}
class SingleByteDecoder {
#index = [];
#fatal = false;
constructor(
index,
{ ignoreBOM = false, fatal = false } = {},
) {
if (ignoreBOM) {
throw new TypeError("Ignoring the BOM is available only with utf-8.");
}
this.#fatal = fatal;
this.#index = index;
}
handler(_stream, byte) {
if (byte === END_OF_STREAM) {
return FINISHED;
}
if (isASCIIByte(byte)) {
return byte;
}
const codePoint = this.#index[byte - 0x80];
if (codePoint == null) {
return decoderError(this.#fatal);
}
return codePoint;
}
}
// The encodingMap is a hash of labels that are indexed by the conical
// encoding.
const encodingMap = {
"windows-1252": [
"ansi_x3.4-1968",
"ascii",
"cp1252",
"cp819",
"csisolatin1",
"ibm819",
"iso-8859-1",
"iso-ir-100",
"iso8859-1",
"iso88591",
"iso_8859-1",
"iso_8859-1:1987",
"l1",
"latin1",
"us-ascii",
"windows-1252",
"x-cp1252",
],
"utf-8": ["unicode-1-1-utf-8", "utf-8", "utf8"],
};
// We convert these into a Map where every label resolves to its canonical
// encoding type.
const encodings = new Map();
for (const key of Object.keys(encodingMap)) {
const labels = encodingMap[key];
for (const label of labels) {
encodings.set(label, key);
}
}
// A map of functions that return new instances of a decoder indexed by the
// encoding type.
const decoders = new Map();
// Single byte decoders are an array of code point lookups
const encodingIndexes = new Map();
// deno-fmt-ignore
encodingIndexes.set("windows-1252", [
8364,
129,
8218,
402,
8222,
8230,
8224,
8225,
710,
8240,
352,
8249,
338,
141,
381,
143,
144,
8216,
8217,
8220,
8221,
8226,
8211,
8212,
732,
8482,
353,
8250,
339,
157,
382,
376,
160,
161,
162,
163,
164,
165,
166,
167,
168,
169,
170,
171,
172,
173,
174,
175,
176,
177,
178,
179,
180,
181,
182,
183,
184,
185,
186,
187,
188,
189,
190,
191,
192,
193,
194,
195,
196,
197,
198,
199,
200,
201,
202,
203,
204,
205,
206,
207,
208,
209,
210,
211,
212,
213,
214,
215,
216,
217,
218,
219,
220,
221,
222,
223,
224,
225,
226,
227,
228,
229,
230,
231,
232,
233,
234,
235,
236,
237,
238,
239,
240,
241,
242,
243,
244,
245,
246,
247,
248,
249,
250,
251,
252,
253,
254,
255,
]);
for (const [key, index] of encodingIndexes) {
decoders.set(
key,
(options) => {
return new SingleByteDecoder(index, options);
},
);
}
function codePointsToString(codePoints) {
let s = "";
for (const cp of codePoints) {
s += String.fromCodePoint(cp);
}
return s;
}
class Stream {
#tokens = [];
constructor(tokens) {
this.#tokens = [...tokens];
this.#tokens.reverse();
}
endOfStream() {
return !this.#tokens.length;
}
read() {
return !this.#tokens.length ? END_OF_STREAM : this.#tokens.pop();
}
prepend(token) {
if (Array.isArray(token)) {
while (token.length) {
this.#tokens.push(token.pop());
}
} else {
this.#tokens.push(token);
}
}
push(token) {
if (Array.isArray(token)) {
while (token.length) {
this.#tokens.unshift(token.shift());
}
} else {
this.#tokens.unshift(token);
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isEitherArrayBuffer(x) {
return x instanceof SharedArrayBuffer || x instanceof ArrayBuffer;
}
class TextDecoder {
#encoding = "";
get encoding() {
return this.#encoding;
}
fatal = false;
ignoreBOM = false;
constructor(label = "utf-8", options = { fatal: false }) {
if (options.ignoreBOM) {
this.ignoreBOM = true;
}
if (options.fatal) {
this.fatal = true;
}
label = String(label).trim().toLowerCase();
const encoding = encodings.get(label);
if (!encoding) {
throw new RangeError(
`The encoding label provided ('${label}') is invalid.`,
);
}
if (!decoders.has(encoding) && encoding !== "utf-8") {
throw new TypeError(`Internal decoder ('${encoding}') not found.`);
}
this.#encoding = encoding;
}
decode(
input,
options = { stream: false },
) {
if (options.stream) {
throw new TypeError("Stream not supported.");
}
let bytes;
if (input instanceof Uint8Array) {
bytes = input;
} else if (isEitherArrayBuffer(input)) {
bytes = new Uint8Array(input);
} else if (
typeof input === "object" &&
"buffer" in input &&
isEitherArrayBuffer(input.buffer)
) {
bytes = new Uint8Array(
input.buffer,
input.byteOffset,
input.byteLength,
);
} else {
bytes = new Uint8Array(0);
}
// For simple utf-8 decoding "Deno.core.decode" can be used for performance
if (
this.#encoding === "utf-8" &&
this.fatal === false &&
this.ignoreBOM === false
) {
return core.decode(bytes);
}
// For performance reasons we utilise a highly optimised decoder instead of
// the general decoder.
if (this.#encoding === "utf-8") {
return decodeUtf8(bytes, this.fatal, this.ignoreBOM);
}
const decoder = decoders.get(this.#encoding)({
fatal: this.fatal,
ignoreBOM: this.ignoreBOM,
});
const inputStream = new Stream(bytes);
const output = [];
while (true) {
const result = decoder.handler(inputStream, inputStream.read());
if (result === FINISHED) {
break;
}
if (result !== CONTINUE) {
output.push(result);
}
}
if (output.length > 0 && output[0] === 0xfeff) {
output.shift();
}
return codePointsToString(output);
}
get [Symbol.toStringTag]() {
return "TextDecoder";
}
}
class TextEncoder {
encoding = "utf-8";
encode(input = "") {
// Deno.core.encode() provides very efficient utf-8 encoding
if (this.encoding === "utf-8") {
return core.encode(input);
}
const encoder = new UTF8Encoder();
const inputStream = new Stream(stringToCodePoints(input));
const output = [];
while (true) {
const result = encoder.handler(inputStream.read());
if (result === "finished") {
break;
}
output.push(...result);
}
return new Uint8Array(output);
}
encodeInto(input, dest) {
const encoder = new UTF8Encoder();
const inputStream = new Stream(stringToCodePoints(input));
let written = 0;
let read = 0;
while (true) {
const result = encoder.handler(inputStream.read());
if (result === "finished") {
break;
}
if (dest.length - written >= result.length) {
read++;
dest.set(result, written);
written += result.length;
if (result.length > 3) {
// increment read a second time if greater than U+FFFF
read++;
}
} else {
break;
}
}
return {
read,
written,
};
}
get [Symbol.toStringTag]() {
return "TextEncoder";
}
}
// This function is based on Bjoern Hoehrmann's DFA UTF-8 decoder.
// See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.
//
// Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
//
// 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.
function decodeUtf8(
input,
fatal,
ignoreBOM,
) {
let outString = "";
// Prepare a buffer so that we don't have to do a lot of string concats, which
// are very slow.
const outBufferLength = Math.min(1024, input.length);
const outBuffer = new Uint16Array(outBufferLength);
let outIndex = 0;
let state = 0;
let codepoint = 0;
let type;
let i =
ignoreBOM && input[0] === 0xef && input[1] === 0xbb && input[2] === 0xbf
? 3
: 0;
for (; i < input.length; ++i) {
// Encoding error handling
if (state === 12 || (state !== 0 && (input[i] & 0xc0) !== 0x80)) {
if (fatal) {
throw new TypeError(
`Decoder error. Invalid byte in sequence at position ${i} in data.`,
);
}
outBuffer[outIndex++] = 0xfffd; // Replacement character
if (outIndex === outBufferLength) {
outString += String.fromCharCode.apply(null, outBuffer);
outIndex = 0;
}
state = 0;
}
// deno-fmt-ignore
type = [
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8
][input[i]];
codepoint = state !== 0
? (input[i] & 0x3f) | (codepoint << 6)
: (0xff >> type) & input[i];
// deno-fmt-ignore
state = [
0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
12,36,12,12,12,12,12,12,12,12,12,12
][state + type];
if (state !== 0) continue;
// Add codepoint to buffer (as charcodes for utf-16), and flush buffer to
// string if needed.
if (codepoint > 0xffff) {
outBuffer[outIndex++] = 0xd7c0 + (codepoint >> 10);
if (outIndex === outBufferLength) {
outString += String.fromCharCode.apply(null, outBuffer);
outIndex = 0;
}
outBuffer[outIndex++] = 0xdc00 | (codepoint & 0x3ff);
if (outIndex === outBufferLength) {
outString += String.fromCharCode.apply(null, outBuffer);
outIndex = 0;
}
} else {
outBuffer[outIndex++] = codepoint;
if (outIndex === outBufferLength) {
outString += String.fromCharCode.apply(null, outBuffer);
outIndex = 0;
}
}
}
// Add a replacement character if we ended in the middle of a sequence or
// encountered an invalid code at the end.
if (state !== 0) {
if (fatal) throw new TypeError(`Decoder error. Unexpected end of data.`);
outBuffer[outIndex++] = 0xfffd; // Replacement character
}
// Final flush of buffer
outString += String.fromCharCode.apply(
null,
outBuffer.subarray(0, outIndex),
);
return outString;
}
// Following code is forked from https://github.com/beatgammit/base64-js
// Copyright (c) 2014 Jameson Little. MIT License.
const lookup = [];
const revLookup = [];
const code =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (let i = 0, len = code.length; i < len; ++i) {
lookup[i] = code[i];
revLookup[code.charCodeAt(i)] = i;
}
// Support decoding URL-safe base64 strings, as Node.js does.
// See: https://en.wikipedia.org/wiki/Base64#URL_applications
revLookup["-".charCodeAt(0)] = 62;
revLookup["_".charCodeAt(0)] = 63;
function getLens(b64) {
const len = b64.length;
if (len % 4 > 0) {
throw new Error("Invalid string. Length must be a multiple of 4");
}
// Trim off extra bytes after placeholder bytes are found
// See: https://github.com/beatgammit/base64-js/issues/42
let validLen = b64.indexOf("=");
if (validLen === -1) validLen = len;
const placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4);
return [validLen, placeHoldersLen];
}
// base64 is 4/3 + up to two characters of the original data
function byteLength(b64) {
const lens = getLens(b64);
const validLen = lens[0];
const placeHoldersLen = lens[1];
return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen;
}
function _byteLength(
b64,
validLen,
placeHoldersLen,
) {
return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen;
}
function toByteArray(b64) {
let tmp;
const lens = getLens(b64);
const validLen = lens[0];
const placeHoldersLen = lens[1];
const arr = new Uint8Array(_byteLength(b64, validLen, placeHoldersLen));
let curByte = 0;
// if there are placeholders, only get up to the last complete 4 chars
const len = placeHoldersLen > 0 ? validLen - 4 : validLen;
let i;
for (i = 0; i < len; i += 4) {
tmp = (revLookup[b64.charCodeAt(i)] << 18) |
(revLookup[b64.charCodeAt(i + 1)] << 12) |
(revLookup[b64.charCodeAt(i + 2)] << 6) |
revLookup[b64.charCodeAt(i + 3)];
arr[curByte++] = (tmp >> 16) & 0xff;
arr[curByte++] = (tmp >> 8) & 0xff;
arr[curByte++] = tmp & 0xff;
}
if (placeHoldersLen === 2) {
tmp = (revLookup[b64.charCodeAt(i)] << 2) |
(revLookup[b64.charCodeAt(i + 1)] >> 4);
arr[curByte++] = tmp & 0xff;
}
if (placeHoldersLen === 1) {
tmp = (revLookup[b64.charCodeAt(i)] << 10) |
(revLookup[b64.charCodeAt(i + 1)] << 4) |
(revLookup[b64.charCodeAt(i + 2)] >> 2);
arr[curByte++] = (tmp >> 8) & 0xff;
arr[curByte++] = tmp & 0xff;
}
return arr;
}
function tripletToBase64(num) {
return (
lookup[(num >> 18) & 0x3f] +
lookup[(num >> 12) & 0x3f] +
lookup[(num >> 6) & 0x3f] +
lookup[num & 0x3f]
);
}
function encodeChunk(uint8, start, end) {
let tmp;
const output = [];
for (let i = start; i < end; i += 3) {
tmp = ((uint8[i] << 16) & 0xff0000) +
((uint8[i + 1] << 8) & 0xff00) +
(uint8[i + 2] & 0xff);
output.push(tripletToBase64(tmp));
}
return output.join("");
}
function fromByteArray(uint8) {
let tmp;
const len = uint8.length;
const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes
const parts = [];
const maxChunkLength = 16383; // must be multiple of 3
// go through the array every three bytes, we'll deal with trailing stuff later
for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
parts.push(
encodeChunk(
uint8,
i,
i + maxChunkLength > len2 ? len2 : i + maxChunkLength,
),
);
}
// pad the end with zeros, but make sure to not forget the extra bytes
if (extraBytes === 1) {
tmp = uint8[len - 1];
parts.push(lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f] + "==");
} else if (extraBytes === 2) {
tmp = (uint8[len - 2] << 8) + uint8[len - 1];
parts.push(
lookup[tmp >> 10] +
lookup[(tmp >> 4) & 0x3f] +
lookup[(tmp << 2) & 0x3f] +
"=",
);
}
return parts.join("");
}
const base64 = {
byteLength,
toByteArray,
fromByteArray,
};
window.TextEncoder = TextEncoder;
window.TextDecoder = TextDecoder;
window.atob = atob;
window.btoa = btoa;
})(this);