diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
| commit | 76cb9c2a39d477a64824a985ade40507e3bbade1 (patch) | |
| tree | 41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/node_modules/@exodus/bytes/fallback/base64.js | |
| parent | 819a39a21ac992b1393244a4c283bbb125208c69 (diff) | |
| download | neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.gz neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.bz2 neko-76cb9c2a39d477a64824a985ade40507e3bbade1.zip | |
feat(vanilla): add testing infrastructure and tests (NK-wjnczv)
Diffstat (limited to 'vanilla/node_modules/@exodus/bytes/fallback/base64.js')
| -rw-r--r-- | vanilla/node_modules/@exodus/bytes/fallback/base64.js | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/vanilla/node_modules/@exodus/bytes/fallback/base64.js b/vanilla/node_modules/@exodus/bytes/fallback/base64.js new file mode 100644 index 0000000..02e4ec8 --- /dev/null +++ b/vanilla/node_modules/@exodus/bytes/fallback/base64.js @@ -0,0 +1,191 @@ +import { nativeEncoder, nativeDecoder } from './platform.js' +import { encodeAscii, decodeAscii } from './latin1.js' + +// See https://datatracker.ietf.org/doc/html/rfc4648 + +const BASE64 = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'] +const BASE64URL = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'] +const BASE64_HELPERS = {} +const BASE64URL_HELPERS = {} + +export const E_CHAR = 'Invalid character in base64 input' +export const E_PADDING = 'Invalid base64 padding' +export const E_LENGTH = 'Invalid base64 length' +export const E_LAST = 'Invalid last chunk' + +// We construct output by concatenating chars, this seems to be fine enough on modern JS engines +// Expects a checked Uint8Array +export function toBase64(arr, isURL, padding) { + const fullChunks = (arr.length / 3) | 0 + const fullChunksBytes = fullChunks * 3 + let o = '' + let i = 0 + + const alphabet = isURL ? BASE64URL : BASE64 + const helpers = isURL ? BASE64URL_HELPERS : BASE64_HELPERS + if (!helpers.pairs) { + helpers.pairs = [] + if (nativeDecoder) { + // Lazy to save memory in case if this is not needed + helpers.codepairs = new Uint16Array(64 * 64) + const u16 = helpers.codepairs + const u8 = new Uint8Array(u16.buffer, u16.byteOffset, u16.byteLength) // write as 1-byte to ignore BE/LE difference + for (let i = 0; i < 64; i++) { + const ic = alphabet[i].charCodeAt(0) + for (let j = 0; j < 64; j++) u8[(i << 7) | (j << 1)] = u8[(j << 7) | ((i << 1) + 1)] = ic + } + } else { + const p = helpers.pairs + for (let i = 0; i < 64; i++) { + for (let j = 0; j < 64; j++) p.push(`${alphabet[i]}${alphabet[j]}`) + } + } + } + + const { pairs, codepairs } = helpers + + // Fast path for complete blocks + // This whole loop can be commented out, the algorithm won't change, it's just an optimization of the next loop + if (nativeDecoder) { + const oa = new Uint16Array(fullChunks * 2) + let j = 0 + for (const last = arr.length - 11; i < last; i += 12, j += 8) { + const x0 = arr[i] + const x1 = arr[i + 1] + const x2 = arr[i + 2] + const x3 = arr[i + 3] + const x4 = arr[i + 4] + const x5 = arr[i + 5] + const x6 = arr[i + 6] + const x7 = arr[i + 7] + const x8 = arr[i + 8] + const x9 = arr[i + 9] + const x10 = arr[i + 10] + const x11 = arr[i + 11] + oa[j] = codepairs[(x0 << 4) | (x1 >> 4)] + oa[j + 1] = codepairs[((x1 & 0x0f) << 8) | x2] + oa[j + 2] = codepairs[(x3 << 4) | (x4 >> 4)] + oa[j + 3] = codepairs[((x4 & 0x0f) << 8) | x5] + oa[j + 4] = codepairs[(x6 << 4) | (x7 >> 4)] + oa[j + 5] = codepairs[((x7 & 0x0f) << 8) | x8] + oa[j + 6] = codepairs[(x9 << 4) | (x10 >> 4)] + oa[j + 7] = codepairs[((x10 & 0x0f) << 8) | x11] + } + + // i < last here is equivalent to i < fullChunksBytes + for (const last = arr.length - 2; i < last; i += 3, j += 2) { + const a = arr[i] + const b = arr[i + 1] + const c = arr[i + 2] + oa[j] = codepairs[(a << 4) | (b >> 4)] + oa[j + 1] = codepairs[((b & 0x0f) << 8) | c] + } + + o = decodeAscii(oa) + } else { + // This can be optimized by ~25% with templates on Hermes, but this codepath is not called on Hermes, it uses btoa + // Check git history for templates version + for (; i < fullChunksBytes; i += 3) { + const a = arr[i] + const b = arr[i + 1] + const c = arr[i + 2] + o += pairs[(a << 4) | (b >> 4)] + o += pairs[((b & 0x0f) << 8) | c] + } + } + + // If we have something left, process it with a full algo + let carry = 0 + let shift = 2 // First byte needs to be shifted by 2 to get 6 bits + const length = arr.length + for (; i < length; i++) { + const x = arr[i] + o += alphabet[carry | (x >> shift)] // shift >= 2, so this fits + if (shift === 6) { + shift = 0 + o += alphabet[x & 0x3f] + } + + carry = (x << (6 - shift)) & 0x3f + shift += 2 // Each byte prints 6 bits and leaves 2 bits + } + + if (shift !== 2) o += alphabet[carry] // shift 2 means we have no carry left + if (padding) o += ['', '==', '='][length - fullChunksBytes] + + return o +} + +// TODO: can this be optimized? This only affects non-Hermes barebone engines though +const mapSize = nativeEncoder ? 128 : 65_536 // we have to store 64 KiB map or recheck everything if we can't decode to byte array + +export function fromBase64(str, isURL) { + let inputLength = str.length + while (str[inputLength - 1] === '=') inputLength-- + const paddingLength = str.length - inputLength + const tailLength = inputLength % 4 + const mainLength = inputLength - tailLength // multiples of 4 + if (tailLength === 1) throw new SyntaxError(E_LENGTH) + if (paddingLength > 3 || (paddingLength !== 0 && str.length % 4 !== 0)) { + throw new SyntaxError(E_PADDING) + } + + const alphabet = isURL ? BASE64URL : BASE64 + const helpers = isURL ? BASE64URL_HELPERS : BASE64_HELPERS + + if (!helpers.fromMap) { + helpers.fromMap = new Int8Array(mapSize).fill(-1) // no regex input validation here, so we map all other bytes to -1 and recheck sign + alphabet.forEach((c, i) => (helpers.fromMap[c.charCodeAt(0)] = i)) + } + + const m = helpers.fromMap + + const arr = new Uint8Array(Math.floor((inputLength * 3) / 4)) + let at = 0 + let i = 0 + + if (nativeEncoder) { + const codes = encodeAscii(str, E_CHAR) + for (; i < mainLength; i += 4) { + const c0 = codes[i] + const c1 = codes[i + 1] + const c2 = codes[i + 2] + const c3 = codes[i + 3] + const a = (m[c0] << 18) | (m[c1] << 12) | (m[c2] << 6) | m[c3] + if (a < 0) throw new SyntaxError(E_CHAR) + arr[at] = a >> 16 + arr[at + 1] = (a >> 8) & 0xff + arr[at + 2] = a & 0xff + at += 3 + } + } else { + for (; i < mainLength; i += 4) { + const c0 = str.charCodeAt(i) + const c1 = str.charCodeAt(i + 1) + const c2 = str.charCodeAt(i + 2) + const c3 = str.charCodeAt(i + 3) + const a = (m[c0] << 18) | (m[c1] << 12) | (m[c2] << 6) | m[c3] + if (a < 0) throw new SyntaxError(E_CHAR) + arr[at] = a >> 16 + arr[at + 1] = (a >> 8) & 0xff + arr[at + 2] = a & 0xff + at += 3 + } + } + + // Can be 0, 2 or 3, verified by padding checks already + if (tailLength < 2) return arr // 0 + const ab = (m[str.charCodeAt(i++)] << 6) | m[str.charCodeAt(i++)] + if (ab < 0) throw new SyntaxError(E_CHAR) + arr[at++] = ab >> 4 + if (tailLength < 3) { + if (ab & 0xf) throw new SyntaxError(E_LAST) + return arr // 2 + } + + const c = m[str.charCodeAt(i++)] + if (c < 0) throw new SyntaxError(E_CHAR) + arr[at++] = ((ab << 4) & 0xff) | (c >> 2) + if (c & 0x3) throw new SyntaxError(E_LAST) + return arr // 3 +} |
