aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/@exodus/bytes/fallback/base64.js
blob: 02e4ec882b7ade0373f37755102bf35d8392ab17 (plain) (blame)
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
183
184
185
186
187
188
189
190
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
}