Skip to content

Commit

Permalink
Optimize base64Write, remove base64-js dependency
Browse files Browse the repository at this point in the history
Also adds support for `base64url` encoding.
  • Loading branch information
chjj committed Feb 4, 2024
1 parent d1a5981 commit 2c5c2df
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 36 deletions.
183 changes: 148 additions & 35 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -393,6 +392,7 @@ Buffer.isEncoding = function isEncoding (encoding) {
case 'latin1':
case 'binary':
case 'base64':
case 'base64url':
case 'ucs2':
case 'ucs-2':
case 'utf16le':
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)

Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"James Halliday <mail@substack.net>"
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
},
"devDependencies": {
Expand Down

0 comments on commit 2c5c2df

Please sign in to comment.