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

buffer: implement buffer.atob and buffer.btoa #37529

Closed
wants to merge 2 commits into from
Closed
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
38 changes: 38 additions & 0 deletions doc/api/buffer.md
Expand Up @@ -3277,6 +3277,44 @@ While, the `Buffer` object is available as a global, there are additional
`Buffer`-related APIs that are available only via the `buffer` module
accessed using `require('buffer')`.

### `buffer.atob(data)`
<!-- YAML
added: REPLACEME
-->

* `data` {any} The Base64-encoded input string.

Decodes a string of Base64-encoded data into bytes, and encodes those bytes
into a string using Latin-1 (ISO-8859-1).

The `data` may be any JavaScript-value that can be coerced into a string.
jasnell marked this conversation as resolved.
Show resolved Hide resolved

**This function is only provided for compatibility with legacy web platform APIs
and should never be used in new code, because they use strings to represent
binary data and predate the introduction of typed arrays in JavaScript.
For code running using Node.js APIs, converting between base64-encoded strings
and binary data should be performed using `Buffer.from(str, 'base64')` and
`buf.toString('base64')`.**

### `buffer.btoa(data)`
<!-- YAML
added: REPLACEME
-->

* `data` {any} An ASCII (Latin1) string.
jasnell marked this conversation as resolved.
Show resolved Hide resolved

Decodes a string into bytes using Latin-1 (ISO-8859), and encodes those bytes
into a string using Base64.

The `data` may be any JavaScript-value that can be coerced into a string.
jasnell marked this conversation as resolved.
Show resolved Hide resolved

**This function is only provided for compatibility with legacy web platform APIs
and should never be used in new code, because they use strings to represent
binary data and predate the introduction of typed arrays in JavaScript.
For code running using Node.js APIs, converting between base64-encoded strings
and binary data should be performed using `Buffer.from(str, 'base64')` and
`buf.toString('base64')`.**

### `buffer.INSPECT_MAX_BYTES`
<!-- YAML
added: v0.5.4
Expand Down
38 changes: 37 additions & 1 deletion lib/buffer.js
Expand Up @@ -1211,14 +1211,50 @@ if (internalBinding('config').hasIntl) {
};
}

let DOMException;

const lazyInvalidCharError = hideStackFrames((message, name) => {
if (DOMException === undefined)
DOMException = internalBinding('messaging').DOMException;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we always have to lazily load this constructor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's gotten a bit out of hand really. I'm doing it for consistency. I recently landed a PR that updated the various AbortError instances to eliminate this in multiple places but there are still more. What would be good is a PR that moves DOMException into `internal/errors' and replaces these lazy loads for that.

throw new DOMException('Invalid character', 'InvalidCharacterError');
});

function btoa(input) {
// TODO(@jasnell): The implementation here has not been performance
// optimized in any way.
input = `${input}`;
for (let n = 0; n < input.length; n++) {
if (input[n].charCodeAt(0) > 0xff)
lazyInvalidCharError();
}
const buf = Buffer.from(input, 'latin1');
return buf.toString('base64');
}

const kBase64Digits =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

function atob(input) {
// TODO(@jasnell): The implementation here has not been performance
// optimized in any way.
input = `${input}`;
for (let n = 0; n < input.length; n++) {
if (!kBase64Digits.includes(input[n]))
lazyInvalidCharError();
}
return Buffer.from(input, 'base64').toString('latin1');
}

module.exports = {
Blob,
Buffer,
SlowBuffer,
transcode,
// Legacy
kMaxLength,
kStringMaxLength
kStringMaxLength,
btoa,
atob,
};

ObjectDefineProperties(module.exports, {
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/wpt/README.md
Expand Up @@ -16,6 +16,7 @@ Last update:
- encoding: https://github.com/web-platform-tests/wpt/tree/35f70910d3/encoding
- FileAPI: https://github.com/web-platform-tests/wpt/tree/3b279420d4/FileAPI
- hr-time: https://github.com/web-platform-tests/wpt/tree/9910784394/hr-time
- html/webappapis/atob: https://github.com/web-platform-tests/wpt/tree/f267e1dca6/html/webappapis/atob
- html/webappapis/microtask-queuing: https://github.com/web-platform-tests/wpt/tree/2c5c3c4c27/html/webappapis/microtask-queuing
- html/webappapis/timers: https://github.com/web-platform-tests/wpt/tree/5873f2d8f1/html/webappapis/timers
- interfaces: https://github.com/web-platform-tests/wpt/tree/79fa4cf76e/interfaces
Expand Down
163 changes: 163 additions & 0 deletions test/fixtures/wpt/html/webappapis/atob/base64.any.js
@@ -0,0 +1,163 @@
/**
* btoa() as defined by the HTML5 spec, which mostly just references RFC4648.
*/
function mybtoa(s) {
// String conversion as required by WebIDL.
s = String(s);

// "The btoa() method must throw an INVALID_CHARACTER_ERR exception if the
// method's first argument contains any character whose code point is
// greater than U+00FF."
for (var i = 0; i < s.length; i++) {
if (s.charCodeAt(i) > 255) {
return "INVALID_CHARACTER_ERR";
}
}

var out = "";
for (var i = 0; i < s.length; i += 3) {
var groupsOfSix = [undefined, undefined, undefined, undefined];
groupsOfSix[0] = s.charCodeAt(i) >> 2;
groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4;
if (s.length > i + 1) {
groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4;
groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2;
}
if (s.length > i + 2) {
groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6;
groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f;
}
for (var j = 0; j < groupsOfSix.length; j++) {
if (typeof groupsOfSix[j] == "undefined") {
out += "=";
} else {
out += btoaLookup(groupsOfSix[j]);
}
}
}
return out;
}

/**
* Lookup table for mybtoa(), which converts a six-bit number into the
* corresponding ASCII character.
*/
function btoaLookup(idx) {
if (idx < 26) {
return String.fromCharCode(idx + 'A'.charCodeAt(0));
}
if (idx < 52) {
return String.fromCharCode(idx - 26 + 'a'.charCodeAt(0));
}
if (idx < 62) {
return String.fromCharCode(idx - 52 + '0'.charCodeAt(0));
}
if (idx == 62) {
return '+';
}
if (idx == 63) {
return '/';
}
// Throw INVALID_CHARACTER_ERR exception here -- won't be hit in the tests.
}

function btoaException(input) {
input = String(input);
for (var i = 0; i < input.length; i++) {
if (input.charCodeAt(i) > 255) {
return true;
}
}
return false;
}

function testBtoa(input) {
// "The btoa() method must throw an INVALID_CHARACTER_ERR exception if the
// method's first argument contains any character whose code point is
// greater than U+00FF."
var normalizedInput = String(input);
for (var i = 0; i < normalizedInput.length; i++) {
if (normalizedInput.charCodeAt(i) > 255) {
assert_throws_dom("InvalidCharacterError", function() { btoa(input); },
"Code unit " + i + " has value " + normalizedInput.charCodeAt(i) + ", which is greater than 255");
return;
}
}
assert_equals(btoa(input), mybtoa(input));
assert_equals(atob(btoa(input)), String(input), "atob(btoa(input)) must be the same as String(input)");
}

var tests = ["עברית", "", "ab", "abc", "abcd", "abcde",
// This one is thrown in because IE9 seems to fail atob(btoa()) on it. Or
// possibly to fail btoa(). I actually can't tell what's happening here,
// but it doesn't hurt.
"\xff\xff\xc0",
// Is your DOM implementation binary-safe?
"\0a", "a\0b",
// WebIDL tests.
undefined, null, 7, 12, 1.5, true, false, NaN, +Infinity, -Infinity, 0, -0,
{toString: function() { return "foo" }},
];
for (var i = 0; i < 258; i++) {
tests.push(String.fromCharCode(i));
}
tests.push(String.fromCharCode(10000));
tests.push(String.fromCharCode(65534));
tests.push(String.fromCharCode(65535));

// This is supposed to be U+10000.
tests.push(String.fromCharCode(0xd800, 0xdc00));
tests = tests.map(
function(elem) {
var expected = mybtoa(elem);
if (expected === "INVALID_CHARACTER_ERR") {
return ["btoa(" + format_value(elem) + ") must raise INVALID_CHARACTER_ERR", elem];
}
return ["btoa(" + format_value(elem) + ") == " + format_value(mybtoa(elem)), elem];
}
);

var everything = "";
for (var i = 0; i < 256; i++) {
everything += String.fromCharCode(i);
}
tests.push(["btoa(first 256 code points concatenated)", everything]);

generate_tests(testBtoa, tests);

promise_test(() => fetch("../../../fetch/data-urls/resources/base64.json").then(res => res.json()).then(runAtobTests), "atob() setup.");

const idlTests = [
[undefined, null],
[null, [158, 233, 101]],
[7, null],
[12, [215]],
[1.5, null],
[true, [182, 187]],
[false, null],
[NaN, [53, 163]],
[+Infinity, [34, 119, 226, 158, 43, 114]],
[-Infinity, null],
[0, null],
[-0, null],
[{toString: function() { return "foo" }}, [126, 138]],
[{toString: function() { return "abcd" }}, [105, 183, 29]]
];

function runAtobTests(tests) {
const allTests = tests.concat(idlTests);
for(let i = 0; i < allTests.length; i++) {
const input = allTests[i][0],
output = allTests[i][1];
test(() => {
if(output === null) {
assert_throws_dom("InvalidCharacterError", () => globalThis.atob(input));
} else {
const result = globalThis.atob(input);
for(let ii = 0; ii < output.length; ii++) {
assert_equals(result.charCodeAt(ii), output[ii]);
}
}
}, "atob(" + format_value(input) + ")");
}
}
4 changes: 4 additions & 0 deletions test/fixtures/wpt/versions.json
Expand Up @@ -23,6 +23,10 @@
"commit": "9910784394858a8e34d9eb4e5d00788765abf837",
"path": "hr-time"
},
"html/webappapis/atob": {
"commit": "f267e1dca6f57a9f4d69f32a6920adfdb3268656",
"path": "html/webappapis/atob"
},
"html/webappapis/microtask-queuing": {
"commit": "2c5c3c4c27d27a419c1fdba3e9879c2d22037074",
"path": "html/webappapis/microtask-queuing"
Expand Down
5 changes: 5 additions & 0 deletions test/wpt/status/html/webappapis/atob.json
@@ -0,0 +1,5 @@
{
"base64.any.js": {
"fail": "promise_test: Unhandled rejection with value: object \"Error: ENOENT: no such file or directory, open '/root/node/node/fetch/data-urls/resources/base64.json'\""
}
}
19 changes: 19 additions & 0 deletions test/wpt/test-atob.js
@@ -0,0 +1,19 @@
'use strict';

require('../common');
const { WPTRunner } = require('../common/wpt');

const runner = new WPTRunner('html/webappapis/atob');

// Needed to access to DOMException.
runner.setFlags(['--expose-internals']);

// Set a script that will be executed in the worker before running the tests.
runner.setInitScript(`
const { internalBinding } = require('internal/test/binding');
const { atob, btoa } = require('buffer');
const { DOMException } = internalBinding('messaging');
global.DOMException = DOMException;
`);

runner.runJsTests();