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