Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#499@minor: Adds support for btoa and atob. #500

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
)
);
});
});
});