Skip to content

Commit

Permalink
capricorn86#499@minor: Adds support for btoa and atob.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mas0nShi committed Jun 9, 2022
1 parent 6be545e commit 453ef14
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/happy-dom/src/exception/DOMExceptionNameEnum.ts
Expand Up @@ -2,6 +2,7 @@ enum DOMExceptionNameEnum {
invalidStateError = 'InvalidStateError',
indexSizeError = 'IndexSizeError',
syntaxError = 'SyntaxError',
hierarchyRequestError = 'HierarchyRequestError'
hierarchyRequestError = 'HierarchyRequestError',
invalidCharacterError = 'InvalidCharacterError'
}
export default DOMExceptionNameEnum;
5 changes: 5 additions & 0 deletions packages/happy-dom/src/window/Window.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
95 changes: 95 additions & 0 deletions 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 = (<string>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 = (<string>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 };
66 changes: 66 additions & 0 deletions packages/happy-dom/test/window/Window.test.ts
Expand Up @@ -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'];

Expand Down Expand Up @@ -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
)
);
});
});
});

0 comments on commit 453ef14

Please sign in to comment.