base64url.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /**
  2. * Avoid modifying this file. It's part of
  3. * https://github.com/briven-community/base64url-js. Submit all fixes on
  4. * that repo!
  5. */
  6. /**
  7. * An array of characters that encode 6 bits into a Base64-URL alphabet
  8. * character.
  9. */
  10. const TO_BASE64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split('')
  11. /**
  12. * An array of characters that can appear in a Base64-URL encoded string but
  13. * should be ignored.
  14. */
  15. const IGNORE_BASE64URL = ' \t\n\r='.split('')
  16. /**
  17. * An array of 128 numbers that map a Base64-URL character to 6 bits, or if -2
  18. * used to skip the character, or if -1 used to error out.
  19. */
  20. const FROM_BASE64URL = (() => {
  21. const charMap: number[] = new Array(128)
  22. for (let i = 0; i < charMap.length; i += 1) {
  23. charMap[i] = -1
  24. }
  25. for (let i = 0; i < IGNORE_BASE64URL.length; i += 1) {
  26. charMap[IGNORE_BASE64URL[i].charCodeAt(0)] = -2
  27. }
  28. for (let i = 0; i < TO_BASE64URL.length; i += 1) {
  29. charMap[TO_BASE64URL[i].charCodeAt(0)] = i
  30. }
  31. return charMap
  32. })()
  33. /**
  34. * Converts a byte to a Base64-URL string.
  35. *
  36. * @param byte The byte to convert, or null to flush at the end of the byte sequence.
  37. * @param state The Base64 conversion state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`.
  38. * @param emit A function called with the next Base64 character when ready.
  39. */
  40. export function byteToBase64URL(
  41. byte: number | null,
  42. state: { queue: number; queuedBits: number },
  43. emit: (char: string) => void
  44. ) {
  45. if (byte !== null) {
  46. state.queue = (state.queue << 8) | byte
  47. state.queuedBits += 8
  48. while (state.queuedBits >= 6) {
  49. const pos = (state.queue >> (state.queuedBits - 6)) & 63
  50. emit(TO_BASE64URL[pos])
  51. state.queuedBits -= 6
  52. }
  53. } else if (state.queuedBits > 0) {
  54. state.queue = state.queue << (6 - state.queuedBits)
  55. state.queuedBits = 6
  56. while (state.queuedBits >= 6) {
  57. const pos = (state.queue >> (state.queuedBits - 6)) & 63
  58. emit(TO_BASE64URL[pos])
  59. state.queuedBits -= 6
  60. }
  61. }
  62. }
  63. /**
  64. * Converts a String char code (extracted using `string.charCodeAt(position)`) to a sequence of Base64-URL characters.
  65. *
  66. * @param charCode The char code of the JavaScript string.
  67. * @param state The Base64 state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`.
  68. * @param emit A function called with the next byte.
  69. */
  70. export function byteFromBase64URL(
  71. charCode: number,
  72. state: { queue: number; queuedBits: number },
  73. emit: (byte: number) => void
  74. ) {
  75. const bits = FROM_BASE64URL[charCode]
  76. if (bits > -1) {
  77. // valid Base64-URL character
  78. state.queue = (state.queue << 6) | bits
  79. state.queuedBits += 6
  80. while (state.queuedBits >= 8) {
  81. emit((state.queue >> (state.queuedBits - 8)) & 0xff)
  82. state.queuedBits -= 8
  83. }
  84. } else if (bits === -2) {
  85. // ignore spaces, tabs, newlines, =
  86. return
  87. } else {
  88. throw new Error(`Invalid Base64-URL character "${String.fromCharCode(charCode)}"`)
  89. }
  90. }
  91. /**
  92. * Converts a JavaScript string (which may include any valid character) into a
  93. * Base64-URL encoded string. The string is first encoded in UTF-8 which is
  94. * then encoded as Base64-URL.
  95. *
  96. * @param str The string to convert.
  97. */
  98. export function stringToBase64URL(str: string) {
  99. const base64: string[] = []
  100. const emitter = (char: string) => {
  101. base64.push(char)
  102. }
  103. const state = { queue: 0, queuedBits: 0 }
  104. stringToUTF8(str, (byte: number) => {
  105. byteToBase64URL(byte, state, emitter)
  106. })
  107. byteToBase64URL(null, state, emitter)
  108. return base64.join('')
  109. }
  110. /**
  111. * Converts a Base64-URL encoded string into a JavaScript string. It is assumed
  112. * that the underlying string has been encoded as UTF-8.
  113. *
  114. * @param str The Base64-URL encoded string.
  115. */
  116. export function stringFromBase64URL(str: string) {
  117. const conv: string[] = []
  118. const utf8Emit = (codepoint: number) => {
  119. conv.push(String.fromCodePoint(codepoint))
  120. }
  121. const utf8State = {
  122. utf8seq: 0,
  123. codepoint: 0,
  124. }
  125. const b64State = { queue: 0, queuedBits: 0 }
  126. const byteEmit = (byte: number) => {
  127. stringFromUTF8(byte, utf8State, utf8Emit)
  128. }
  129. for (let i = 0; i < str.length; i += 1) {
  130. byteFromBase64URL(str.charCodeAt(i), b64State, byteEmit)
  131. }
  132. return conv.join('')
  133. }
  134. /**
  135. * Converts a Unicode codepoint to a multi-byte UTF-8 sequence.
  136. *
  137. * @param codepoint The Unicode codepoint.
  138. * @param emit Function which will be called for each UTF-8 byte that represents the codepoint.
  139. */
  140. export function codepointToUTF8(codepoint: number, emit: (byte: number) => void) {
  141. if (codepoint <= 0x7f) {
  142. emit(codepoint)
  143. return
  144. } else if (codepoint <= 0x7ff) {
  145. emit(0xc0 | (codepoint >> 6))
  146. emit(0x80 | (codepoint & 0x3f))
  147. return
  148. } else if (codepoint <= 0xffff) {
  149. emit(0xe0 | (codepoint >> 12))
  150. emit(0x80 | ((codepoint >> 6) & 0x3f))
  151. emit(0x80 | (codepoint & 0x3f))
  152. return
  153. } else if (codepoint <= 0x10ffff) {
  154. emit(0xf0 | (codepoint >> 18))
  155. emit(0x80 | ((codepoint >> 12) & 0x3f))
  156. emit(0x80 | ((codepoint >> 6) & 0x3f))
  157. emit(0x80 | (codepoint & 0x3f))
  158. return
  159. }
  160. throw new Error(`Unrecognized Unicode codepoint: ${codepoint.toString(16)}`)
  161. }
  162. /**
  163. * Converts a JavaScript string to a sequence of UTF-8 bytes.
  164. *
  165. * @param str The string to convert to UTF-8.
  166. * @param emit Function which will be called for each UTF-8 byte of the string.
  167. */
  168. export function stringToUTF8(str: string, emit: (byte: number) => void) {
  169. for (let i = 0; i < str.length; i += 1) {
  170. let codepoint = str.charCodeAt(i)
  171. if (codepoint > 0xd7ff && codepoint <= 0xdbff) {
  172. // most UTF-16 codepoints are Unicode codepoints, except values in this
  173. // range where the next UTF-16 codepoint needs to be combined with the
  174. // current one to get the Unicode codepoint
  175. const highSurrogate = ((codepoint - 0xd800) * 0x400) & 0xffff
  176. const lowSurrogate = (str.charCodeAt(i + 1) - 0xdc00) & 0xffff
  177. codepoint = (lowSurrogate | highSurrogate) + 0x10000
  178. i += 1
  179. }
  180. codepointToUTF8(codepoint, emit)
  181. }
  182. }
  183. /**
  184. * Converts a UTF-8 byte to a Unicode codepoint.
  185. *
  186. * @param byte The UTF-8 byte next in the sequence.
  187. * @param state The shared state between consecutive UTF-8 bytes in the
  188. * sequence, an object with the shape `{ utf8seq: 0, codepoint: 0 }`.
  189. * @param emit Function which will be called for each codepoint.
  190. */
  191. export function stringFromUTF8(
  192. byte: number,
  193. state: { utf8seq: number; codepoint: number },
  194. emit: (codepoint: number) => void
  195. ) {
  196. if (state.utf8seq === 0) {
  197. if (byte <= 0x7f) {
  198. emit(byte)
  199. return
  200. }
  201. // count the number of 1 leading bits until you reach 0
  202. for (let leadingBit = 1; leadingBit < 6; leadingBit += 1) {
  203. if (((byte >> (7 - leadingBit)) & 1) === 0) {
  204. state.utf8seq = leadingBit
  205. break
  206. }
  207. }
  208. if (state.utf8seq === 2) {
  209. state.codepoint = byte & 31
  210. } else if (state.utf8seq === 3) {
  211. state.codepoint = byte & 15
  212. } else if (state.utf8seq === 4) {
  213. state.codepoint = byte & 7
  214. } else {
  215. throw new Error('Invalid UTF-8 sequence')
  216. }
  217. state.utf8seq -= 1
  218. } else if (state.utf8seq > 0) {
  219. if (byte <= 0x7f) {
  220. throw new Error('Invalid UTF-8 sequence')
  221. }
  222. state.codepoint = (state.codepoint << 6) | (byte & 63)
  223. state.utf8seq -= 1
  224. if (state.utf8seq === 0) {
  225. emit(state.codepoint)
  226. }
  227. }
  228. }