86 行
3.3 KiB
JavaScript
86 行
3.3 KiB
JavaScript
|
|
'use strict'
|
||
|
|
|
||
|
|
const STANDARD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||
|
|
const URL_SAFE_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 纯 JS Base64 编码,默认输出带填充的标准 Base64。
|
||
|
|
* 协议调用方必须显式声明 URL-safe 与是否保留填充,避免现场字符串替换。
|
||
|
|
*/
|
||
|
|
function encodeBase64(value, options = {}) {
|
||
|
|
if (!(value instanceof Uint8Array)) throw new TypeError('Base64 value must be Uint8Array')
|
||
|
|
const alphabet = options.urlSafe ? URL_SAFE_ALPHABET : STANDARD_ALPHABET
|
||
|
|
const padding = options.padding !== false
|
||
|
|
let output = ''
|
||
|
|
for (let offset = 0; offset < value.length; offset += 3) {
|
||
|
|
const first = value[offset]
|
||
|
|
const hasSecond = offset + 1 < value.length
|
||
|
|
const hasThird = offset + 2 < value.length
|
||
|
|
const second = hasSecond ? value[offset + 1] : 0
|
||
|
|
const third = hasThird ? value[offset + 2] : 0
|
||
|
|
output += alphabet[first >>> 2]
|
||
|
|
output += alphabet[((first & 0x03) << 4) | (second >>> 4)]
|
||
|
|
if (hasSecond) {
|
||
|
|
output += alphabet[((second & 0x0f) << 2) | (third >>> 6)]
|
||
|
|
} else if (padding) {
|
||
|
|
output += '='
|
||
|
|
}
|
||
|
|
if (hasThird) {
|
||
|
|
output += alphabet[third & 0x3f]
|
||
|
|
} else if (padding) {
|
||
|
|
output += '='
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return output
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 严格解码标准 Base64 或 Base64URL,不依赖浏览器/Hermes 的 atob。
|
||
|
|
* 接受有填充或无填充形式,但拒绝混合字母表、非法长度、非法填充和非零尾随位。
|
||
|
|
*/
|
||
|
|
function decodeBase64(value) {
|
||
|
|
if (typeof value !== 'string') throw new TypeError('Base64 value must be a string')
|
||
|
|
if (value.length === 0) return new Uint8Array(0)
|
||
|
|
if (/[^A-Za-z0-9+/_=-]/.test(value)) throw new Error('Base64 contains invalid characters')
|
||
|
|
if (/[+\/]/.test(value) && /[-_]/.test(value)) {
|
||
|
|
throw new Error('Base64 cannot mix standard and URL-safe alphabets')
|
||
|
|
}
|
||
|
|
|
||
|
|
const firstPadding = value.indexOf('=')
|
||
|
|
const data = firstPadding === -1 ? value : value.slice(0, firstPadding)
|
||
|
|
const padding = firstPadding === -1 ? '' : value.slice(firstPadding)
|
||
|
|
if (padding !== '' && padding !== '=' && padding !== '==') {
|
||
|
|
throw new Error('Base64 padding is invalid')
|
||
|
|
}
|
||
|
|
if (data.includes('=')) throw new Error('Base64 padding is invalid')
|
||
|
|
const remainder = data.length % 4
|
||
|
|
if (remainder === 1) throw new Error('Base64 length is invalid')
|
||
|
|
const expectedPadding = remainder === 0 ? 0 : 4 - remainder
|
||
|
|
if (padding.length !== 0 && padding.length !== expectedPadding) {
|
||
|
|
throw new Error('Base64 padding length is invalid')
|
||
|
|
}
|
||
|
|
|
||
|
|
const normalized = data.replace(/-/g, '+').replace(/_/g, '/')
|
||
|
|
const outputLength = Math.floor((normalized.length * 6) / 8)
|
||
|
|
const output = new Uint8Array(outputLength)
|
||
|
|
let accumulator = 0
|
||
|
|
let bitCount = 0
|
||
|
|
let outputIndex = 0
|
||
|
|
for (const character of normalized) {
|
||
|
|
const sextet = STANDARD_ALPHABET.indexOf(character)
|
||
|
|
if (sextet < 0) throw new Error('Base64 contains invalid characters')
|
||
|
|
accumulator = (accumulator << 6) | sextet
|
||
|
|
bitCount += 6
|
||
|
|
if (bitCount >= 8) {
|
||
|
|
bitCount -= 8
|
||
|
|
output[outputIndex] = (accumulator >>> bitCount) & 0xff
|
||
|
|
outputIndex += 1
|
||
|
|
accumulator &= (1 << bitCount) - 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (accumulator !== 0) throw new Error('Base64 has non-zero trailing bits')
|
||
|
|
return output
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { decodeBase64, encodeBase64 }
|