diff --git a/README.md b/README.md index ce2be0e..c3b3a80 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,46 @@ for (let byte of buffer) { const string = result.join(""); ``` + +### Uint8Array and Base64-URL + +To convert a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) to Base64-URL: + +```typescript +import { byteToBase64URL } from "./base64url"; + +let bytes: Uint8Array; + +const result: string[] = []; +const state = { queue: 0, queuedBits: 0 }; + +const onChar = (char: string) => { + result.push(char); +}; + +bytes.map((byte) => byteToBase64URL(byte, state, onChar)); + +// always call with `null` after processing all bytes +byteToBase64URL(null, state, onChar); + +const string = result.join(""); +``` + +To convert Base64-URL to a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array): + +```typescript +import { byteFromBase64URL } from "./base64url"; + +const result: number[] = []; +const state = { queue: 0, queuedBits: 0 }; + +const onByte = (byte: number) => { + result.push(byte); +}; + +for (let i = 0; i < string.length; i += 1) { + byteFromBase64URL(string.charCodeAt(i), state, onByte); +} + +const bytes = new Uint8Array(result); +``` diff --git a/src/base64url.test.ts b/src/base64url.test.ts index 4a38601..95848f7 100644 --- a/src/base64url.test.ts +++ b/src/base64url.test.ts @@ -41,7 +41,7 @@ describe("stringFromBase64URL", () => { test("decode with invalid Base64-URL character", () => { expect(() => { stringFromBase64URL("*"); - }).toThrow(new Error(`Invalid Base64-URL character "*" at position 0`)); + }).toThrow(new Error(`Invalid Base64-URL character "*"`)); }); }); diff --git a/src/base64url.ts b/src/base64url.ts index 6e4bca4..e2d2dd7 100644 --- a/src/base64url.ts +++ b/src/base64url.ts @@ -39,6 +39,72 @@ const FROM_BASE64URL = (() => { return charMap; })(); +/** + * Converts a byte to a Base64-URL string. + * + * @param byte The byte to convert, or null to flush at the end of the byte sequence. + * @param state The Base64 conversion state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`. + * @param emit A function called with the next Base64 character when ready. + */ +export function byteToBase64URL( + byte: number | null, + state: { queue: number; queuedBits: number }, + emit: (char: string) => void, +) { + if (byte !== null) { + state.queue = (state.queue << 8) | byte; + state.queuedBits += 8; + + while (state.queuedBits >= 6) { + const pos = (state.queue >> (state.queuedBits - 6)) & 63; + emit(TO_BASE64URL[pos]); + state.queuedBits -= 6; + } + } else if (state.queuedBits > 0) { + state.queue = state.queue << (6 - state.queuedBits); + state.queuedBits = 6; + + while (state.queuedBits >= 6) { + const pos = (state.queue >> (state.queuedBits - 6)) & 63; + emit(TO_BASE64URL[pos]); + state.queuedBits -= 6; + } + } +} + +/** + * Converts a String char code (extracted using `string.charCodeAt(position)`) to a sequence of Base64-URL characters. + * + * @param charCode The char code of the JavaScript string. + * @param state The Base64 state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`. + * @param emit A function called with the next byte. + */ +export function byteFromBase64URL( + charCode: number, + state: { queue: number; queuedBits: number }, + emit: (byte: number) => void, +) { + const bits = FROM_BASE64URL[charCode]; + + if (bits > -1) { + // valid Base64-URL character + state.queue = (state.queue << 6) | bits; + state.queuedBits += 6; + + while (state.queuedBits >= 8) { + emit((state.queue >> (state.queuedBits - 8)) & 0xff); + state.queuedBits -= 8; + } + } else if (bits === -2) { + // ignore spaces, tabs, newlines, = + return; + } else { + throw new Error( + `Invalid Base64-URL character "${String.fromCharCode(charCode)}"`, + ); + } +} + /** * Converts a JavaScript string (which may include any valid character) into a * Base64-URL encoded string. The string is first encoded in UTF-8 which is @@ -49,32 +115,17 @@ const FROM_BASE64URL = (() => { export function stringToBase64URL(str: string) { const base64: string[] = []; - let queue = 0; - let queuedBits = 0; - - const emitter = (byte: number) => { - queue = (queue << 8) | byte; - queuedBits += 8; - - while (queuedBits >= 6) { - const pos = (queue >> (queuedBits - 6)) & 63; - base64.push(TO_BASE64URL[pos]); - queuedBits -= 6; - } + const emitter = (char: string) => { + base64.push(char); }; - stringToUTF8(str, emitter); + const state = { queue: 0, queuedBits: 0 }; - if (queuedBits > 0) { - queue = queue << (6 - queuedBits); - queuedBits = 6; + stringToUTF8(str, (byte: number) => { + byteToBase64URL(byte, state, emitter); + }); - while (queuedBits >= 6) { - const pos = (queue >> (queuedBits - 6)) & 63; - base64.push(TO_BASE64URL[pos]); - queuedBits -= 6; - } - } + byteToBase64URL(null, state, emitter); return base64.join(""); } @@ -88,39 +139,23 @@ export function stringToBase64URL(str: string) { export function stringFromBase64URL(str: string) { const conv: string[] = []; - const emit = (codepoint: number) => { + const utf8Emit = (codepoint: number) => { conv.push(String.fromCodePoint(codepoint)); }; - const state = { + const utf8State = { utf8seq: 0, codepoint: 0, }; - let queue = 0; - let queuedBits = 0; - - for (let i = 0; i < str.length; i += 1) { - const codepoint = str.charCodeAt(i); - const bits = FROM_BASE64URL[codepoint]; + const b64State = { queue: 0, queuedBits: 0 }; - if (bits > -1) { - // valid Base64-URL character - queue = (queue << 6) | bits; - queuedBits += 6; + const byteEmit = (byte: number) => { + stringFromUTF8(byte, utf8State, utf8Emit); + }; - while (queuedBits >= 8) { - stringFromUTF8((queue >> (queuedBits - 8)) & 0xff, state, emit); - queuedBits -= 8; - } - } else if (bits === -2) { - // ignore spaces, tabs, newlines, = - continue; - } else { - throw new Error( - `Invalid Base64-URL character "${str.at(i)}" at position ${i}`, - ); - } + for (let i = 0; i < str.length; i += 1) { + byteFromBase64URL(str.charCodeAt(i), b64State, byteEmit); } return conv.join("");