From 2c5c2df47fa1a36848283d7fa749eb0b36d6235f Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sun, 4 Feb 2024 12:21:31 -0500 Subject: [PATCH] Optimize base64Write, remove base64-js dependency Also adds support for `base64url` encoding. --- index.js | 183 +++++++++++++++++++++++++++++++++++++++++---------- package.json | 1 - 2 files changed, 148 insertions(+), 36 deletions(-) diff --git a/index.js b/index.js index a6d1b10..e62a446 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,6 @@ 'use strict' -const base64 = require('base64-js') const ieee754 = require('ieee754') const customInspectSymbol = (typeof Symbol === 'function' && typeof Symbol['for'] === 'function') // eslint-disable-line dot-notation @@ -393,6 +392,7 @@ Buffer.isEncoding = function isEncoding (encoding) { case 'latin1': case 'binary': case 'base64': + case 'base64url': case 'ucs2': case 'ucs-2': case 'utf16le': @@ -484,7 +484,8 @@ function byteLength (string, encoding) { case 'hex': return len >>> 1 case 'base64': - return base64ToBytes(string).length + case 'base64url': + return base64ByteLength(string, len) default: if (loweredCase) { return mustMatch ? -1 : utf8ByteLength(string) // assume utf8 @@ -550,7 +551,10 @@ function slowToString (encoding, start, end) { return latin1Slice(this, start, end) case 'base64': - return base64Slice(this, start, end) + return base64Slice(this, start, end, base64Charset, true) + + case 'base64url': + return base64Slice(this, start, end, base64UrlCharset, false) case 'ucs2': case 'ucs-2': @@ -1015,7 +1019,62 @@ function asciiWrite (buf, string, offset, length) { } function base64Write (buf, string, offset, length) { - return blitBuffer(base64ToBytes(string), buf, offset, length) + const src = string.replace(/[^+/0-9A-Za-z-_=]/g, '') + const eq = src.indexOf('=') + const dst = buf + + let srcLen = eq >= 0 ? eq : src.length + let srcPos = 0 + let dstLen = length + let dstPos = offset + + while (srcLen >= 4 && dstLen >= 3) { + const t1 = base64Table[src.charCodeAt(srcPos++)] + const t2 = base64Table[src.charCodeAt(srcPos++)] + const t3 = base64Table[src.charCodeAt(srcPos++)] + const t4 = base64Table[src.charCodeAt(srcPos++)] + + dst[dstPos++] = (t1 << 2) | (t2 >> 4) + dst[dstPos++] = (t2 << 4) | (t3 >> 2) + dst[dstPos++] = (t3 << 6) | (t4 >> 0) + + srcLen -= 4 + dstLen -= 3 + } + + { + let w1, w2, w3, w4 + + while (srcLen && dstLen) { + const w = base64Table[src.charCodeAt(srcPos)] + + switch (srcPos & 3) { + case 0: + w1 = w + break + case 1: + w2 = w + dst[dstPos++] = (w1 << 2) | (w2 >> 4) + dstLen-- + break + case 2: + w3 = w + dst[dstPos++] = (w2 << 4) | (w3 >> 2) + dstLen-- + break + case 3: + w4 = w + dst[dstPos++] = (w3 << 6) | (w4 >> 0) + dstLen-- + break + } + + srcPos++ + srcLen-- + } + } + + return dstPos - offset } function ucs2Write (buf, string, offset, length) { @@ -1091,6 +1150,7 @@ Buffer.prototype.write = function write (string, offset, length, encoding) { return asciiWrite(this, string, offset, length) case 'base64': + case 'base64url': // Warning: maxLength not taken into account in base64Write return base64Write(this, string, offset, length) @@ -1115,12 +1175,53 @@ Buffer.prototype.toJSON = function toJSON () { } } -function base64Slice (buf, start, end) { - if (start === 0 && end === buf.length) { - return base64.fromByteArray(buf) - } else { - return base64.fromByteArray(buf.slice(start, end)) +function base64Slice (buf, start, end, charset, pad) { + end = Math.min(buf.length, end) + + let left = end - start + let i = start + let str = '' + + while (left >= 3) { + const c1 = buf[i++] + const c2 = buf[i++] + const c3 = buf[i++] + + str += charset[c1 >> 2] + str += charset[((c1 & 3) << 4) | (c2 >> 4)] + str += charset[((c2 & 15) << 2) | (c3 >> 6)] + str += charset[c3 & 63] + + left -= 3 + } + + switch (left) { + case 1: { + const c1 = buf[i++] + + str += charset[c1 >> 2] + str += charset[(c1 & 3) << 4] + + if (pad) str += '==' + + break + } + + case 2: { + const c1 = buf[i++] + const c2 = buf[i++] + + str += charset[c1 >> 2] + str += charset[((c1 & 3) << 4) | (c2 >> 4)] + str += charset[(c2 & 15) << 2] + + if (pad) str += '=' + + break + } } + + return str } function utf8Slice (buf, start, end) { @@ -2109,26 +2210,6 @@ function boundsError (value, length, type) { // HELPER FUNCTIONS // ================ -const INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g - -function base64clean (str) { - // Node takes equal signs as end of the Base64 encoding - str = str.split('=')[0] - // Node strips out invalid characters like \n and \t from the string, base64-js does not - str = str.trim().replace(INVALID_BASE64_RE, '') - // Node converts strings with length < 2 to '' - if (str.length < 2) return '' - // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not - while (str.length % 4 !== 0) { - str = str + '=' - } - return str -} - -function base64ToBytes (str) { - return base64.toByteArray(base64clean(str)) -} - function writeInvalid (buf, pos) { // U+FFFD (Replacement Character) buf[pos++] = 0xef @@ -2137,13 +2218,18 @@ function writeInvalid (buf, pos) { return pos } -function blitBuffer (src, dst, offset, length) { - let i - for (i = 0; i < length; ++i) { - if ((i + offset >= dst.length) || (i >= src.length)) break - dst[i + offset] = src[i] +function base64ByteLength (str, bytes) { + // Handle padding + if (bytes > 0 && str.charCodeAt(bytes - 1) === 0x3d) { + bytes-- + } + + if (bytes > 1 && str.charCodeAt(bytes - 1) === 0x3d) { + bytes-- } - return i + + // Base64 ratio: 3/4 + return (bytes * 3) >>> 2 } // ArrayBuffer or Uint8Array objects from other contexts (i.e. iframes) do not pass @@ -2195,6 +2281,33 @@ const hexCharValueTable = [ ] /* eslint-enable no-multi-spaces, indent */ +const base64Charset = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + +const base64UrlCharset = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_' + +/* eslint-disable no-multi-spaces, indent */ +const base64Table = [ + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, 62, -1, 62, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, -1, -1, -1, -1, -1 +] +/* eslint-enable no-multi-spaces, indent */ + // Return not function with Error if BigInt not supported function defineBigIntMethod (fn) { return typeof BigInt === 'undefined' ? BufferBigIntNotDefined : fn diff --git a/package.json b/package.json index 5ee6b8d..93b695e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "James Halliday " ], "dependencies": { - "base64-js": "^1.3.1", "ieee754": "^1.2.1" }, "devDependencies": {