From 0746394d76b05563b7c6e0ea6230aaf182fe2aba Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 5 Dec 2023 02:30:31 +0100 Subject: [PATCH] =?UTF-8?q?feat(util):=20=F0=9F=8E=B8=20add=20Base64url=20?= =?UTF-8?q?encoder=20and=20decoder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base64/__tests__/decode-base64url.spec.ts | 20 ++++++++++++++++ .../base64/__tests__/encode-base64url.spec.ts | 23 +++++++++++++++++++ src/util/base64/createFromBase64.ts | 14 +++++++++-- src/util/base64/createToBase64.ts | 5 +--- src/util/base64/fromBase64Url.ts | 3 +++ src/util/base64/toBase64Url.ts | 3 +++ 6 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/util/base64/__tests__/decode-base64url.spec.ts create mode 100644 src/util/base64/__tests__/encode-base64url.spec.ts create mode 100644 src/util/base64/fromBase64Url.ts create mode 100644 src/util/base64/toBase64Url.ts diff --git a/src/util/base64/__tests__/decode-base64url.spec.ts b/src/util/base64/__tests__/decode-base64url.spec.ts new file mode 100644 index 0000000000..3344250fc3 --- /dev/null +++ b/src/util/base64/__tests__/decode-base64url.spec.ts @@ -0,0 +1,20 @@ +import {toBase64} from '../toBase64'; +import {fromBase64Url} from '../fromBase64Url'; + +const generateBlob = (): Uint8Array => { + const length = Math.floor(Math.random() * 100); + const uint8 = new Uint8Array(length); + for (let i = 0; i < length; i++) { + uint8[i] = Math.floor(Math.random() * 256); + } + return uint8; +}; + +test('works', () => { + for (let i = 0; i < 100; i++) { + const blob = generateBlob(); + const encoded = toBase64(blob).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + const decoded2 = fromBase64Url(encoded); + expect(decoded2).toEqual(blob); + } +}); diff --git a/src/util/base64/__tests__/encode-base64url.spec.ts b/src/util/base64/__tests__/encode-base64url.spec.ts new file mode 100644 index 0000000000..1d2880f21b --- /dev/null +++ b/src/util/base64/__tests__/encode-base64url.spec.ts @@ -0,0 +1,23 @@ +import {toBase64Url} from '../toBase64Url'; + +const generateBlob = (): Uint8Array => { + const length = Math.floor(Math.random() * 100) + 1; + const uint8 = new Uint8Array(length); + for (let i = 0; i < length; i++) { + uint8[i] = Math.floor(Math.random() * 256); + } + return uint8; +}; + +test('works', () => { + for (let i = 0; i < 100; i++) { + const blob = generateBlob(); + const expected = Buffer.from(blob).toString('base64'); + const base64url = toBase64Url(blob, blob.length); + let encoded = base64url.replace(/-/g, '+').replace(/_/g, '/'); + const mod = encoded.length % 4; + if (mod === 2) encoded += '=='; + else if (mod === 3) encoded += '='; + expect(encoded).toEqual(expected); + } +}); diff --git a/src/util/base64/createFromBase64.ts b/src/util/base64/createFromBase64.ts index e528434622..5f4922f4e4 100644 --- a/src/util/base64/createFromBase64.ts +++ b/src/util/base64/createFromBase64.ts @@ -2,7 +2,7 @@ import {alphabet} from './constants'; const E = '='; -export const createFromBase64 = (chars: string = alphabet) => { +export const createFromBase64 = (chars: string = alphabet, noPadding: boolean = false) => { if (chars.length !== 64) throw new Error('chars must be 64 characters long'); let max = 0; for (let i = 0; i < chars.length; i++) max = Math.max(max, chars.charCodeAt(i)); @@ -12,7 +12,17 @@ export const createFromBase64 = (chars: string = alphabet) => { return (encoded: string): Uint8Array => { if (!encoded) return new Uint8Array(0); - const length = encoded.length; + let length = encoded.length; + if (noPadding) { + const mod = length % 4; + if (mod === 2) { + encoded += '=='; + length += 2; + } else if (mod === 3) { + encoded += '='; + length += 1; + } + } if (length % 4 !== 0) throw new Error('Base64 string length must be a multiple of 4'); const mainLength = encoded[length - 1] !== E ? length : length - 4; let bufferLength = (length >> 2) * 3; diff --git a/src/util/base64/createToBase64.ts b/src/util/base64/createToBase64.ts index fdbce4984f..6a8b8e62e1 100644 --- a/src/util/base64/createToBase64.ts +++ b/src/util/base64/createToBase64.ts @@ -1,9 +1,6 @@ import {alphabet} from './constants'; -const E = '='; -const EE = '=='; - -export const createToBase64 = (chars: string = alphabet) => { +export const createToBase64 = (chars: string = alphabet, E: string = '=', EE: string = '==') => { if (chars.length !== 64) throw new Error('chars must be 64 characters long'); const table = chars.split(''); diff --git a/src/util/base64/fromBase64Url.ts b/src/util/base64/fromBase64Url.ts new file mode 100644 index 0000000000..12be59f819 --- /dev/null +++ b/src/util/base64/fromBase64Url.ts @@ -0,0 +1,3 @@ +import {createFromBase64} from './createFromBase64'; + +export const fromBase64Url = createFromBase64('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', true); diff --git a/src/util/base64/toBase64Url.ts b/src/util/base64/toBase64Url.ts new file mode 100644 index 0000000000..c638705583 --- /dev/null +++ b/src/util/base64/toBase64Url.ts @@ -0,0 +1,3 @@ +import {createToBase64} from './createToBase64'; + +export const toBase64Url = createToBase64('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', '', '');