From 453ef14bae25952fc4ceb95b7777eb036894bc7e Mon Sep 17 00:00:00 2001 From: Mas0nShi Date: Thu, 9 Jun 2022 17:28:23 +0800 Subject: [PATCH] #499@minor: Adds support for btoa and atob. --- .../src/exception/DOMExceptionNameEnum.ts | 3 +- packages/happy-dom/src/window/Window.ts | 5 + packages/happy-dom/src/window/WindowBase64.ts | 95 +++++++++++++++++++ packages/happy-dom/test/window/Window.test.ts | 66 +++++++++++++ 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 packages/happy-dom/src/window/WindowBase64.ts diff --git a/packages/happy-dom/src/exception/DOMExceptionNameEnum.ts b/packages/happy-dom/src/exception/DOMExceptionNameEnum.ts index 04e773d78..aa051467d 100644 --- a/packages/happy-dom/src/exception/DOMExceptionNameEnum.ts +++ b/packages/happy-dom/src/exception/DOMExceptionNameEnum.ts @@ -2,6 +2,7 @@ enum DOMExceptionNameEnum { invalidStateError = 'InvalidStateError', indexSizeError = 'IndexSizeError', syntaxError = 'SyntaxError', - hierarchyRequestError = 'HierarchyRequestError' + hierarchyRequestError = 'HierarchyRequestError', + invalidCharacterError = 'InvalidCharacterError' } export default DOMExceptionNameEnum; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index b42a04b9a..a2d63cd4a 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -92,6 +92,7 @@ import VMGlobalPropertyScript from './VMGlobalPropertyScript'; import * as PerfHooks from 'perf_hooks'; import VM from 'vm'; import { Buffer } from 'buffer'; +import { atob, btoa } from './WindowBase64'; /** * Browser window. @@ -219,6 +220,10 @@ export default class Window extends EventTarget implements IWindow { public readonly localStorage = new Storage(); public readonly performance = PerfHooks.performance; + // Atob & btoa + public atob = atob; + public btoa = btoa; + // Node.js Globals public ArrayBuffer; public Boolean; diff --git a/packages/happy-dom/src/window/WindowBase64.ts b/packages/happy-dom/src/window/WindowBase64.ts new file mode 100644 index 000000000..c9ad7b650 --- /dev/null +++ b/packages/happy-dom/src/window/WindowBase64.ts @@ -0,0 +1,95 @@ +import DOMException from '../exception/DOMException'; +import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum'; + +const base64list = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + +/** + * Btoa. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/btoa. + * + * @param data + */ +export const btoa = (data: unknown): string => { + const str = (data).toString(); + if (/[^\u0000-\u00ff]/.test(str)) { + throw new DOMException( + "Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.", + DOMExceptionNameEnum.invalidCharacterError + ); + } + + let t = ''; + let p = -6; + let a = 0; + let i = 0; + let v = 0; + let c; + while (i < str.length || p > -6) { + if (p < 0) { + if (i < str.length) { + c = str.charCodeAt(i++); + v += 8; + } else { + c = 0; + } + a = ((a & 255) << 8) | (c & 255); + p += 8; + } + t += base64list.charAt(v > 0 ? (a >> p) & 63 : 64); + p -= 6; + v -= 6; + } + return t; +}; + +/** + * Atob. + * + * Reference: + * https://infra.spec.whatwg.org/#forgiving-base64-encode. + * Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. + * + * @param data + */ +export const atob = (data: unknown): string => { + const str = (data).toString(); + + if (/[^\u0000-\u00ff]/.test(str)) { + throw new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of the Latin1 range.", + DOMExceptionNameEnum.invalidCharacterError + ); + } + + if (/[^A-Za-z\d+/=]/.test(str) || str.length % 4 == 1) { + throw new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", + DOMExceptionNameEnum.invalidCharacterError + ); + } + + let t = ''; + let p = -8; + let a = 0; + let c; + let d; + for (let i = 0; i < str.length; i++) { + if ((c = base64list.indexOf(str.charAt(i))) < 0) { + continue; + } + a = (a << 6) | (c & 63); + if ((p += 6) >= 0) { + d = (a >> p) & 255; + if (c !== 64) { + t += String.fromCharCode(d); + } + a &= 63; + p -= 8; + } + } + return t; +}; + +exports = { btoa: btoa, atob: atob }; diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index fe547d16f..299b9b072 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -11,6 +11,9 @@ import Headers from '../../src/fetch/Headers'; import Response from '../../src/fetch/Response'; import Request from '../../src/fetch/Request'; import Selection from '../../src/selection/Selection'; +import { atob, btoa } from '../../lib/window/WindowBase64'; +import DOMException from '../../src/exception/DOMException'; +import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum'; const MOCKED_NODE_FETCH = global['mockedModules']['node-fetch']; @@ -535,4 +538,67 @@ describe('Window', () => { }, 0); }); }); + + describe('atob()', () => { + it('Decode "hello my happy dom!"', function () { + const encoded = 'aGVsbG8gbXkgaGFwcHkgZG9tIQ=='; + const decoded = atob(encoded); + expect(decoded).toBe('hello my happy dom!'); + }); + + it('Decode Unicode (throw error)', function () { + expect(() => { + const data = '😄 hello my happy dom! 🐛'; + atob(data); + }).toThrowError( + new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of the Latin1 range.", + DOMExceptionNameEnum.invalidCharacterError + ) + ); + }); + + it('Data not in base64list', function () { + expect(() => { + const data = '\x11GVsbG8gbXkgaGFwcHkgZG9tIQ=='; + atob(data); + }).toThrowError( + new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", + DOMExceptionNameEnum.invalidCharacterError + ) + ); + }); + it('Data length not valid', function () { + expect(() => { + const data = 'aGVsbG8gbXkgaGFwcHkgZG9tI'; + atob(data); + }).toThrowError( + new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", + DOMExceptionNameEnum.invalidCharacterError + ) + ); + }); + }); + + describe('btoa()', () => { + it('Encode "hello my happy dom!"', function () { + const data = 'hello my happy dom!'; + const encoded = btoa(data); + expect(encoded).toBe('aGVsbG8gbXkgaGFwcHkgZG9tIQ=='); + }); + + it('Encode Unicode (throw error)', function () { + expect(() => { + const data = '😄 hello my happy dom! 🐛'; + btoa(data); + }).toThrowError( + new DOMException( + "Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.", + DOMExceptionNameEnum.invalidCharacterError + ) + ); + }); + }); });