Skip to content

Commit 61582b4

Browse files
committed
Use atob/btoa for base64, remove base64-js dependency
1 parent 39d2fa9 commit 61582b4

File tree

3 files changed

+163
-31
lines changed

3 files changed

+163
-31
lines changed

index.js

+116-30
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
'use strict'
1010

11-
const base64 = require('base64-js')
1211
const ieee754 = require('ieee754')
1312
const customInspectSymbol =
1413
(typeof Symbol === 'function' && typeof Symbol['for'] === 'function') // eslint-disable-line dot-notation
@@ -38,10 +37,54 @@ exports.constants = {
3837
MAX_STRING_LENGTH: K_STRING_MAX_LENGTH
3938
}
4039

41-
exports.Blob = global.Blob
42-
exports.File = global.File
43-
exports.atob = global.atob
44-
exports.btoa = global.btoa
40+
exports.Blob = typeof Blob !== 'undefined' ? Blob : undefined
41+
exports.File = typeof File !== 'undefined' ? File : undefined
42+
exports.atob = typeof atob !== 'undefined' ? atob : undefined
43+
exports.btoa = typeof btoa !== 'undefined' ? btoa : undefined
44+
45+
/**
46+
* The `atob` and `btoa` functions are unoptimized in node.js[1][2].
47+
* As a result of this, we call out to Buffer directly when running
48+
* inside of node.js. Unfortunately, detecting node.js is tricky:
49+
*
50+
* We can't check `process.browser` because it will cause browserify
51+
* to pull in the entire `process` module.
52+
*
53+
* Instead, we check for a global `Buffer` object with `asciiSlice`
54+
* defined on the prototype. This undocumented method has been
55+
* defined on the node.js Buffer prototype since the _very_ early
56+
* days of node.js (as early as 0.4.0) and is still defined to this
57+
* day (but is not defined on _our_ Buffer prototype).
58+
*
59+
* Because our `Buffer` constructor is hoisted, we can't check for
60+
* `typeof Buffer === 'function'`. Instead, we need to access `global`.
61+
*
62+
* Unfortunately, we can't assume `global` exists as there may be a
63+
* non-browserify bundler which supports CJS but not a full node.js
64+
* environment which includes `global`.
65+
*
66+
* As an added bonus, this hack also accounts for nodes prior to
67+
* v16.0.0 (when `atob` and `btoa` were first exposed globally).
68+
*
69+
* [1] https://github.com/feross/buffer/issues/339
70+
* [2] https://github.com/nodejs/node/pull/38433
71+
*/
72+
let _atob = exports.atob
73+
let _btoa = exports.btoa
74+
75+
if (typeof global !== 'undefined' && global && global.Math === Math &&
76+
typeof global.Buffer === 'function' && global.Buffer.prototype &&
77+
typeof global.Buffer.prototype.asciiSlice === 'function') {
78+
const NodeBuffer = global.Buffer
79+
80+
_atob = function atob (str) {
81+
return NodeBuffer.from(str, 'base64').toString('binary')
82+
}
83+
84+
_btoa = function btoa (str) {
85+
return NodeBuffer.from(str, 'binary').toString('base64')
86+
}
87+
}
4588

4689
/**
4790
* If `Buffer.TYPED_ARRAY_SUPPORT`:
@@ -398,6 +441,7 @@ Buffer.isEncoding = function isEncoding (encoding) {
398441
case 'latin1':
399442
case 'binary':
400443
case 'base64':
444+
case 'base64url':
401445
case 'ucs2':
402446
case 'ucs-2':
403447
case 'utf16le':
@@ -489,7 +533,8 @@ function byteLength (string, encoding) {
489533
case 'hex':
490534
return len >>> 1
491535
case 'base64':
492-
return base64ToBytes(string).length
536+
case 'base64url':
537+
return base64ByteLength(string, len)
493538
default:
494539
if (loweredCase) {
495540
return mustMatch ? -1 : utf8ByteLength(string) // assume utf8
@@ -557,6 +602,9 @@ function slowToString (encoding, start, end) {
557602
case 'base64':
558603
return base64Slice(this, start, end)
559604

605+
case 'base64url':
606+
return base64UrlSlice(this, start, end)
607+
560608
case 'ucs2':
561609
case 'ucs-2':
562610
case 'utf16le':
@@ -1020,7 +1068,14 @@ function asciiWrite (buf, string, offset, length) {
10201068
}
10211069

10221070
function base64Write (buf, string, offset, length) {
1023-
return blitBuffer(base64ToBytes(string), buf, offset, length)
1071+
try {
1072+
// Parse optimistically as base64.
1073+
string = _atob(string)
1074+
} catch (e) {
1075+
// Fall back to full preprocessing.
1076+
string = _atob(base64clean(string))
1077+
}
1078+
return asciiWrite(buf, string, offset, length)
10241079
}
10251080

10261081
function ucs2Write (buf, string, offset, length) {
@@ -1096,6 +1151,7 @@ Buffer.prototype.write = function write (string, offset, length, encoding) {
10961151
return asciiWrite(this, string, offset, length)
10971152

10981153
case 'base64':
1154+
case 'base64url':
10991155
// Warning: maxLength not taken into account in base64Write
11001156
return base64Write(this, string, offset, length)
11011157

@@ -1121,11 +1177,11 @@ Buffer.prototype.toJSON = function toJSON () {
11211177
}
11221178

11231179
function base64Slice (buf, start, end) {
1124-
if (start === 0 && end === buf.length) {
1125-
return base64.fromByteArray(buf)
1126-
} else {
1127-
return base64.fromByteArray(buf.slice(start, end))
1128-
}
1180+
return _btoa(latin1Slice(buf, start, end))
1181+
}
1182+
1183+
function base64UrlSlice (buf, start, end) {
1184+
return base64convert(base64Slice(buf, start, end))
11291185
}
11301186

11311187
function utf8Slice (buf, start, end) {
@@ -2109,23 +2165,62 @@ function boundsError (value, length, type) {
21092165
// ================
21102166

21112167
const INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g
2168+
const BASE64URL_62 = /-/g
2169+
const BASE64URL_63 = /_/g
21122170

21132171
function base64clean (str) {
2114-
// Node takes equal signs as end of the Base64 encoding
2115-
str = str.split('=')[0]
2116-
// Node strips out invalid characters like \n and \t from the string, base64-js does not
2117-
str = str.trim().replace(INVALID_BASE64_RE, '')
2172+
// Node takes equal signs as end of the encoding
2173+
const index = str.indexOf('=')
2174+
2175+
if (index >= 0) {
2176+
str = str.slice(0, index)
2177+
}
2178+
2179+
// Node strips out invalid characters, atob does not
2180+
str = str.replace(INVALID_BASE64_RE, '')
2181+
21182182
// Node converts strings with length < 2 to ''
21192183
if (str.length < 2) return ''
2120-
// Node allows for non-padded base64 strings (missing trailing ===), base64-js does not
2121-
while (str.length % 4 !== 0) {
2122-
str = str + '='
2184+
2185+
// Node handles base64-url, atob does not
2186+
str = str.replace(BASE64URL_62, '+')
2187+
str = str.replace(BASE64URL_63, '/')
2188+
2189+
// Node allows for non-padded strings, atob _may_ not
2190+
while (str.length & 3) {
2191+
str += '='
2192+
}
2193+
2194+
return str
2195+
}
2196+
2197+
const BASE64_62 = /\+/g
2198+
const BASE64_63 = /\//g
2199+
2200+
function base64convert (str) {
2201+
// Convert base64 to base64-url.
2202+
let len = str.length
2203+
2204+
if (len > 0 && str[len - 1] === '=') len--
2205+
if (len > 0 && str[len - 1] === '=') len--
2206+
2207+
if (len !== str.length) {
2208+
str = str.slice(0, len)
21232209
}
2210+
2211+
str = str.replace(BASE64_62, '-')
2212+
str = str.replace(BASE64_63, '_')
2213+
21242214
return str
21252215
}
21262216

2127-
function base64ToBytes (str) {
2128-
return base64.toByteArray(base64clean(str))
2217+
function base64ByteLength (str, bytes) {
2218+
// Handle padding
2219+
if (bytes > 0 && str.charCodeAt(bytes - 1) === 0x3d) bytes--
2220+
if (bytes > 1 && str.charCodeAt(bytes - 1) === 0x3d) bytes--
2221+
2222+
// Base64 ratio: 3/4
2223+
return (bytes * 3) >>> 2
21292224
}
21302225

21312226
function writeInvalid (buf, pos) {
@@ -2136,15 +2231,6 @@ function writeInvalid (buf, pos) {
21362231
return pos
21372232
}
21382233

2139-
function blitBuffer (src, dst, offset, length) {
2140-
let i
2141-
for (i = 0; i < length; ++i) {
2142-
if ((i + offset >= dst.length) || (i >= src.length)) break
2143-
dst[i + offset] = src[i]
2144-
}
2145-
return i
2146-
}
2147-
21482234
// ArrayBuffer or Uint8Array objects from other contexts (i.e. iframes) do not pass
21492235
// the `instanceof` check but they should be treated as of that type.
21502236
// See: https://github.com/feross/buffer/issues/166

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
"James Halliday <[email protected]>"
1717
],
1818
"dependencies": {
19-
"base64-js": "^1.3.1",
2019
"ieee754": "^1.2.1"
2120
},
2221
"devDependencies": {

test/base64.js

+47
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,50 @@ test('base64: high byte', function (t) {
5353
)
5454
t.end()
5555
})
56+
57+
test('base64: rfc test vectors', function (t) {
58+
// https://tools.ietf.org/html/rfc4648#section-10
59+
const vectors = [
60+
['', ''],
61+
['66', 'Zg=='],
62+
['666f', 'Zm8='],
63+
['666f6f', 'Zm9v'],
64+
['666f6f62', 'Zm9vYg=='],
65+
['666f6f6261', 'Zm9vYmE='],
66+
['666f6f626172', 'Zm9vYmFy'],
67+
['53e9363b2962fcaf', 'U+k2Oyli/K8=']
68+
]
69+
70+
for (const [base16, base64] of vectors) {
71+
const buf16 = B.from(base16, 'hex')
72+
const buf64 = B.from(base64, 'base64')
73+
74+
t.equal(buf16.toString('base64'), base64)
75+
t.equal(buf64.toString('hex'), base16)
76+
}
77+
78+
t.end()
79+
})
80+
81+
test('base64url: rfc test vectors', function (t) {
82+
const vectors = [
83+
['', ''],
84+
['66', 'Zg'],
85+
['666f', 'Zm8'],
86+
['666f6f', 'Zm9v'],
87+
['666f6f62', 'Zm9vYg'],
88+
['666f6f6261', 'Zm9vYmE'],
89+
['666f6f626172', 'Zm9vYmFy'],
90+
['53e9363b2962fcaf', 'U-k2Oyli_K8']
91+
]
92+
93+
for (const [base16, base64] of vectors) {
94+
const buf16 = B.from(base16, 'hex')
95+
const buf64 = B.from(base64, 'base64url')
96+
97+
t.equal(buf16.toString('base64url'), base64)
98+
t.equal(buf64.toString('hex'), base16)
99+
}
100+
101+
t.end()
102+
})

0 commit comments

Comments
 (0)