Spaces:
Paused
Paused
File size: 8,495 Bytes
3401f26 | 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 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | import { assertEmptyRest } from './assert.js'
import { assertU8, fromUint8, fromBuffer, E_STRING } from './fallback/_utils.js'
import { isHermes } from './fallback/platform.js'
import { decodeLatin1, encodeLatin1 } from './fallback/latin1.js'
import * as js from './fallback/base64.js'
// See https://datatracker.ietf.org/doc/html/rfc4648
// base64: A-Za-z0-9+/ and = if padding not disabled
// base64url: A-Za-z0-9_- and = if padding enabled
const { Buffer, atob, btoa } = globalThis // Buffer is optional, only used when native
const haveNativeBuffer = Buffer && !Buffer.TYPED_ARRAY_SUPPORT
const { toBase64: web64 } = Uint8Array.prototype // Modern engines have this
const { E_CHAR, E_PADDING, E_LENGTH, E_LAST } = js
// faster only on Hermes (and a little in old Chrome), js path beats it on normal engines
const shouldUseBtoa = btoa && isHermes
const shouldUseAtob = atob && isHermes
// For native Buffer codepaths only
const isBuffer = (x) => x.constructor === Buffer && Buffer.isBuffer(x)
const toBuffer = (x) => (isBuffer(x) ? x : Buffer.from(x.buffer, x.byteOffset, x.byteLength))
function maybeUnpad(res, padding) {
if (padding) return res
const at = res.indexOf('=', res.length - 3)
return at === -1 ? res : res.slice(0, at)
}
function maybePad(res, padding) {
return padding && res.length % 4 !== 0 ? res + '='.repeat(4 - (res.length % 4)) : res
}
const toUrl = (x) => x.replaceAll('+', '-').replaceAll('/', '_')
const haveWeb = (x) => web64 && x.toBase64 === web64
export function toBase64(x, { padding = true } = {}) {
assertU8(x)
if (haveWeb(x)) return padding ? x.toBase64() : x.toBase64({ omitPadding: !padding }) // Modern, optionless is slightly faster
if (haveNativeBuffer) return maybeUnpad(toBuffer(x).base64Slice(0, x.byteLength), padding) // Older Node.js
if (shouldUseBtoa) return maybeUnpad(btoa(decodeLatin1(x)), padding)
return js.toBase64(x, false, padding) // Fallback
}
// NOTE: base64url omits padding by default
export function toBase64url(x, { padding = false } = {}) {
assertU8(x)
if (haveWeb(x)) return x.toBase64({ alphabet: 'base64url', omitPadding: !padding }) // Modern
if (haveNativeBuffer) return maybePad(toBuffer(x).base64urlSlice(0, x.byteLength), padding) // Older Node.js
if (shouldUseBtoa) return maybeUnpad(toUrl(btoa(decodeLatin1(x))), padding)
return js.toBase64(x, true, padding) // Fallback
}
// Unlike Buffer.from(), throws on invalid input (non-base64 symbols and incomplete chunks)
// Unlike Buffer.from() and Uint8Array.fromBase64(), does not allow spaces
// NOTE: Always operates in strict mode for last chunk
// By default accepts both padded and non-padded variants, only strict base64
export function fromBase64(str, options) {
if (typeof options === 'string') options = { format: options } // Compat due to usage, TODO: remove
if (!options) return fromBase64common(str, false, 'both', 'uint8', null)
const { format = 'uint8', padding = 'both', ...rest } = options
return fromBase64common(str, false, padding, format, rest)
}
// By default accepts only non-padded strict base64url
export function fromBase64url(str, options) {
if (!options) return fromBase64common(str, true, false, 'uint8', null)
const { format = 'uint8', padding = false, ...rest } = options
return fromBase64common(str, true, padding, format, rest)
}
// By default accepts both padded and non-padded variants, base64 or base64url
export function fromBase64any(str, { format = 'uint8', padding = 'both', ...rest } = {}) {
if (typeof str !== 'string') throw new TypeError(E_STRING)
const isBase64url = !str.includes('+') && !str.includes('/') // likely to fail fast, as most input is non-url, also double scan is faster than regex
return fromBase64common(str, isBase64url, padding, format, rest)
}
function fromBase64common(str, isBase64url, padding, format, rest) {
if (typeof str !== 'string') throw new TypeError(E_STRING)
if (rest !== null) assertEmptyRest(rest)
const auto = padding === 'both' ? str.endsWith('=') : undefined
// Older JSC supporting Uint8Array.fromBase64 lacks proper checks
if (padding === true || auto === true) {
if (str.length % 4 !== 0) throw new SyntaxError(E_PADDING) // JSC misses this
if (str[str.length - 3] === '=') throw new SyntaxError(E_PADDING) // no more than two = at the end
} else if (padding === false || auto === false) {
if (str.length % 4 === 1) throw new SyntaxError(E_LENGTH) // JSC misses this in fromBase64
if (padding === false && str.endsWith('=')) {
throw new SyntaxError('Did not expect padding in base64 input') // inclusion is checked separately
}
} else {
throw new TypeError('Invalid padding option')
}
return fromBase64impl(str, isBase64url, padding, format)
}
// ASCII whitespace is U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE
const ASCII_WHITESPACE = /[\t\n\f\r ]/ // non-u for JSC perf
function noWhitespaceSeen(str, arr) {
const at = str.indexOf('=', str.length - 3)
const paddingLength = at >= 0 ? str.length - at : 0
const chars = str.length - paddingLength
const e = chars % 4 // extra chars past blocks of 4
const b = arr.length - ((chars - e) / 4) * 3 // remaining bytes not covered by full blocks of chars
return (e === 0 && b === 0) || (e === 2 && b === 1) || (e === 3 && b === 2)
}
let fromBase64impl
if (Uint8Array.fromBase64) {
// NOTICE: this is actually slower than our JS impl in older JavaScriptCore and (slightly) in SpiderMonkey, but faster on V8 and new JavaScriptCore
fromBase64impl = (str, isBase64url, padding, format) => {
const alphabet = isBase64url ? 'base64url' : 'base64'
let arr
if (padding === true) {
// Padding is required from user, and we already checked that string length is divisible by 4
// Padding might still be wrong due to whitespace, but in that case native impl throws expected error
arr = Uint8Array.fromBase64(str, { alphabet, lastChunkHandling: 'strict' })
} else {
try {
const padded = str.length % 4 > 0 ? `${str}${'='.repeat(4 - (str.length % 4))}` : str
arr = Uint8Array.fromBase64(padded, { alphabet, lastChunkHandling: 'strict' })
} catch (err) {
// Normalize error: whitespace in input could have caused added padding to be invalid
// But reporting that as a padding error would be confusing
throw ASCII_WHITESPACE.test(str) ? new SyntaxError(E_CHAR) : err
}
}
// We don't allow whitespace in input, but that can be rechecked based on output length
// All other chars are checked natively
if (!noWhitespaceSeen(str, arr)) throw new SyntaxError(E_CHAR)
return fromUint8(arr, format)
}
} else if (haveNativeBuffer) {
fromBase64impl = (str, isBase64url, padding, format) => {
const size = Buffer.byteLength(str, 'base64')
const arr = Buffer.allocUnsafeSlow(size) // non-pooled
if (arr.base64Write(str) !== size) throw new SyntaxError(E_PADDING)
// Rechecking by re-encoding is cheaper than regexes on Node.js
const got = isBase64url ? maybeUnpad(str, padding === false) : maybePad(str, padding !== true)
const valid = isBase64url ? arr.base64urlSlice(0, arr.length) : arr.base64Slice(0, arr.length)
if (got !== valid) throw new SyntaxError(E_PADDING)
return fromBuffer(arr, format) // fully checked
}
} else if (shouldUseAtob) {
// atob is faster than manual parsing on Hermes
fromBase64impl = (str, isBase64url, padding, format) => {
let arr
if (isBase64url) {
if (/[\t\n\f\r +/]/.test(str)) throw new SyntaxError(E_CHAR) // atob verifies other invalid input
str = str.replaceAll('-', '+').replaceAll('_', '/') // from url to normal
}
try {
arr = encodeLatin1(atob(str))
} catch {
throw new SyntaxError(E_CHAR) // convert atob errors
}
if (!isBase64url && !noWhitespaceSeen(str, arr)) throw new SyntaxError(E_CHAR) // base64url checks input above
if (arr.length % 3 !== 0) {
// Check last chunk to be strict if it was incomplete
const expected = toBase64(arr.subarray(-(arr.length % 3))) // str is normalized to non-url already
const end = str.length % 4 === 0 ? str.slice(-4) : str.slice(-(str.length % 4)).padEnd(4, '=')
if (expected !== end) throw new SyntaxError(E_LAST)
}
return fromUint8(arr, format)
}
} else {
fromBase64impl = (str, isBase64url, padding, format) =>
fromUint8(js.fromBase64(str, isBase64url), format) // validated in js
}
|