base64.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { nativeEncoder, nativeDecoder } from './platform.js'
  2. import { encodeAscii, decodeAscii } from './latin1.js'
  3. // See https://datatracker.ietf.org/doc/html/rfc4648
  4. const BASE64 = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/']
  5. const BASE64URL = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_']
  6. const BASE64_HELPERS = {}
  7. const BASE64URL_HELPERS = {}
  8. export const E_CHAR = 'Invalid character in base64 input'
  9. export const E_PADDING = 'Invalid base64 padding'
  10. export const E_LENGTH = 'Invalid base64 length'
  11. export const E_LAST = 'Invalid last chunk'
  12. // We construct output by concatenating chars, this seems to be fine enough on modern JS engines
  13. // Expects a checked Uint8Array
  14. export function toBase64(arr, isURL, padding) {
  15. const fullChunks = (arr.length / 3) | 0
  16. const fullChunksBytes = fullChunks * 3
  17. let o = ''
  18. let i = 0
  19. const alphabet = isURL ? BASE64URL : BASE64
  20. const helpers = isURL ? BASE64URL_HELPERS : BASE64_HELPERS
  21. if (!helpers.pairs) {
  22. helpers.pairs = []
  23. if (nativeDecoder) {
  24. // Lazy to save memory in case if this is not needed
  25. helpers.codepairs = new Uint16Array(64 * 64)
  26. const u16 = helpers.codepairs
  27. const u8 = new Uint8Array(u16.buffer, u16.byteOffset, u16.byteLength) // write as 1-byte to ignore BE/LE difference
  28. for (let i = 0; i < 64; i++) {
  29. const ic = alphabet[i].charCodeAt(0)
  30. for (let j = 0; j < 64; j++) u8[(i << 7) | (j << 1)] = u8[(j << 7) | ((i << 1) + 1)] = ic
  31. }
  32. } else {
  33. const p = helpers.pairs
  34. for (let i = 0; i < 64; i++) {
  35. for (let j = 0; j < 64; j++) p.push(`${alphabet[i]}${alphabet[j]}`)
  36. }
  37. }
  38. }
  39. const { pairs, codepairs } = helpers
  40. // Fast path for complete blocks
  41. // This whole loop can be commented out, the algorithm won't change, it's just an optimization of the next loop
  42. if (nativeDecoder) {
  43. const oa = new Uint16Array(fullChunks * 2)
  44. let j = 0
  45. for (const last = arr.length - 11; i < last; i += 12, j += 8) {
  46. const x0 = arr[i]
  47. const x1 = arr[i + 1]
  48. const x2 = arr[i + 2]
  49. const x3 = arr[i + 3]
  50. const x4 = arr[i + 4]
  51. const x5 = arr[i + 5]
  52. const x6 = arr[i + 6]
  53. const x7 = arr[i + 7]
  54. const x8 = arr[i + 8]
  55. const x9 = arr[i + 9]
  56. const x10 = arr[i + 10]
  57. const x11 = arr[i + 11]
  58. oa[j] = codepairs[(x0 << 4) | (x1 >> 4)]
  59. oa[j + 1] = codepairs[((x1 & 0x0f) << 8) | x2]
  60. oa[j + 2] = codepairs[(x3 << 4) | (x4 >> 4)]
  61. oa[j + 3] = codepairs[((x4 & 0x0f) << 8) | x5]
  62. oa[j + 4] = codepairs[(x6 << 4) | (x7 >> 4)]
  63. oa[j + 5] = codepairs[((x7 & 0x0f) << 8) | x8]
  64. oa[j + 6] = codepairs[(x9 << 4) | (x10 >> 4)]
  65. oa[j + 7] = codepairs[((x10 & 0x0f) << 8) | x11]
  66. }
  67. // i < last here is equivalent to i < fullChunksBytes
  68. for (const last = arr.length - 2; i < last; i += 3, j += 2) {
  69. const a = arr[i]
  70. const b = arr[i + 1]
  71. const c = arr[i + 2]
  72. oa[j] = codepairs[(a << 4) | (b >> 4)]
  73. oa[j + 1] = codepairs[((b & 0x0f) << 8) | c]
  74. }
  75. o = decodeAscii(oa)
  76. } else {
  77. // This can be optimized by ~25% with templates on Hermes, but this codepath is not called on Hermes, it uses btoa
  78. // Check git history for templates version
  79. for (; i < fullChunksBytes; i += 3) {
  80. const a = arr[i]
  81. const b = arr[i + 1]
  82. const c = arr[i + 2]
  83. o += pairs[(a << 4) | (b >> 4)]
  84. o += pairs[((b & 0x0f) << 8) | c]
  85. }
  86. }
  87. // If we have something left, process it with a full algo
  88. let carry = 0
  89. let shift = 2 // First byte needs to be shifted by 2 to get 6 bits
  90. const length = arr.length
  91. for (; i < length; i++) {
  92. const x = arr[i]
  93. o += alphabet[carry | (x >> shift)] // shift >= 2, so this fits
  94. if (shift === 6) {
  95. shift = 0
  96. o += alphabet[x & 0x3f]
  97. }
  98. carry = (x << (6 - shift)) & 0x3f
  99. shift += 2 // Each byte prints 6 bits and leaves 2 bits
  100. }
  101. if (shift !== 2) o += alphabet[carry] // shift 2 means we have no carry left
  102. if (padding) o += ['', '==', '='][length - fullChunksBytes]
  103. return o
  104. }
  105. // TODO: can this be optimized? This only affects non-Hermes barebone engines though
  106. const mapSize = nativeEncoder ? 128 : 65_536 // we have to store 64 KiB map or recheck everything if we can't decode to byte array
  107. export function fromBase64(str, isURL) {
  108. let inputLength = str.length
  109. while (str[inputLength - 1] === '=') inputLength--
  110. const paddingLength = str.length - inputLength
  111. const tailLength = inputLength % 4
  112. const mainLength = inputLength - tailLength // multiples of 4
  113. if (tailLength === 1) throw new SyntaxError(E_LENGTH)
  114. if (paddingLength > 3 || (paddingLength !== 0 && str.length % 4 !== 0)) {
  115. throw new SyntaxError(E_PADDING)
  116. }
  117. const alphabet = isURL ? BASE64URL : BASE64
  118. const helpers = isURL ? BASE64URL_HELPERS : BASE64_HELPERS
  119. if (!helpers.fromMap) {
  120. helpers.fromMap = new Int8Array(mapSize).fill(-1) // no regex input validation here, so we map all other bytes to -1 and recheck sign
  121. alphabet.forEach((c, i) => (helpers.fromMap[c.charCodeAt(0)] = i))
  122. }
  123. const m = helpers.fromMap
  124. const arr = new Uint8Array(Math.floor((inputLength * 3) / 4))
  125. let at = 0
  126. let i = 0
  127. if (nativeEncoder) {
  128. const codes = encodeAscii(str, E_CHAR)
  129. for (; i < mainLength; i += 4) {
  130. const c0 = codes[i]
  131. const c1 = codes[i + 1]
  132. const c2 = codes[i + 2]
  133. const c3 = codes[i + 3]
  134. const a = (m[c0] << 18) | (m[c1] << 12) | (m[c2] << 6) | m[c3]
  135. if (a < 0) throw new SyntaxError(E_CHAR)
  136. arr[at] = a >> 16
  137. arr[at + 1] = (a >> 8) & 0xff
  138. arr[at + 2] = a & 0xff
  139. at += 3
  140. }
  141. } else {
  142. for (; i < mainLength; i += 4) {
  143. const c0 = str.charCodeAt(i)
  144. const c1 = str.charCodeAt(i + 1)
  145. const c2 = str.charCodeAt(i + 2)
  146. const c3 = str.charCodeAt(i + 3)
  147. const a = (m[c0] << 18) | (m[c1] << 12) | (m[c2] << 6) | m[c3]
  148. if (a < 0) throw new SyntaxError(E_CHAR)
  149. arr[at] = a >> 16
  150. arr[at + 1] = (a >> 8) & 0xff
  151. arr[at + 2] = a & 0xff
  152. at += 3
  153. }
  154. }
  155. // Can be 0, 2 or 3, verified by padding checks already
  156. if (tailLength < 2) return arr // 0
  157. const ab = (m[str.charCodeAt(i++)] << 6) | m[str.charCodeAt(i++)]
  158. if (ab < 0) throw new SyntaxError(E_CHAR)
  159. arr[at++] = ab >> 4
  160. if (tailLength < 3) {
  161. if (ab & 0xf) throw new SyntaxError(E_LAST)
  162. return arr // 2
  163. }
  164. const c = m[str.charCodeAt(i++)]
  165. if (c < 0) throw new SyntaxError(E_CHAR)
  166. arr[at++] = ((ab << 4) & 0xff) | (c >> 2)
  167. if (c & 0x3) throw new SyntaxError(E_LAST)
  168. return arr // 3
  169. }