-
-
Notifications
You must be signed in to change notification settings - Fork 32k
buffer: implement buffer.atob and buffer.btoa #37529
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1211,14 +1211,50 @@ if (internalBinding('config').hasIntl) { | |
}; | ||
} | ||
|
||
let DOMException; | ||
|
||
const lazyInvalidCharError = hideStackFrames((message, name) => { | ||
if (DOMException === undefined) | ||
DOMException = internalBinding('messaging').DOMException; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we always have to lazily load this constructor? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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, { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) + ")"); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'\"" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); |
Uh oh!
There was an error while loading. Please reload this page.