Skip to content

Commit

Permalink
Merge pull request #65 from gtm-nayan/perf-stringify
Browse files Browse the repository at this point in the history
perf: speed up stringify_string
  • Loading branch information
Rich-Harris committed May 14, 2023
2 parents 99e66d0 + ffc41d5 commit c8239e4
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 48 deletions.
2 changes: 1 addition & 1 deletion src/uneval.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from './utils.js';

const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
const unsafe_chars = /[<>\b\f\n\r\t\0\u2028\u2029]/g;
const unsafe_chars = /[<\b\f\n\r\t\0\u2028\u2029]/g;
const reserved =
/^(?:do|if|in|for|int|let|new|try|var|byte|case|char|else|enum|goto|long|this|void|with|await|break|catch|class|const|final|float|short|super|throw|while|yield|delete|double|export|import|native|return|switch|throws|typeof|boolean|default|extends|finally|package|private|abstract|continue|debugger|function|volatile|interface|protected|transient|implements|instanceof|synchronized)$/;

Expand Down
72 changes: 43 additions & 29 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
/** @type {Record<string, string>} */
export const escaped = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\0': '\\u0000',
'\u2028': '\\u2028',
'\u2029': '\\u2029'
};
Expand All @@ -31,7 +28,9 @@ export function is_primitive(thing) {
return Object(thing) !== thing;
}

const object_proto_names = Object.getOwnPropertyNames(Object.prototype)
const object_proto_names = /* @__PURE__ */ Object.getOwnPropertyNames(
Object.prototype
)
.sort()
.join('\0');

Expand All @@ -51,35 +50,50 @@ export function get_type(thing) {
return Object.prototype.toString.call(thing).slice(8, -1);
}

/** @param {string} char */
function get_escaped_char(char) {
switch (char) {
case '"':
return '\\"';
case '<':
return '\\u003C';
case '\\':
return '\\\\';
case '\n':
return '\\n';
case '\r':
return '\\r';
case '\t':
return '\\t';
case '\b':
return '\\b';
case '\f':
return '\\f';
case '\u2028':
return '\\u2028';
case '\u2029':
return '\\u2029';
default:
return char < ' '
? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`
: '';
}
}

/** @param {string} str */
export function stringify_string(str) {
let result = '"';

for (let i = 0; i < str.length; i += 1) {
const char = str.charAt(i);
const code = char.charCodeAt(0);

if (char === '"') {
result += '\\"';
} else if (char in escaped) {
result += escaped[char];
} else if (code <= 0x001F) {
result += `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`
} else if (code >= 0xd800 && code <= 0xdfff) {
const next = str.charCodeAt(i + 1);
let result = '';
let last_pos = 0;
const len = str.length;

// If this is the beginning of a [high, low] surrogate pair,
// add the next two characters, otherwise escape
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
result += char + str[++i];
} else {
result += `\\u${code.toString(16).toUpperCase()}`;
}
} else {
result += char;
for (let i = 0; i < len; i += 1) {
const char = str[i];
const replacement = get_escaped_char(char);
if (replacement) {
result += str.slice(last_pos, i) + replacement;
last_pos = i + 1;
}
}

result += '"';
return result;
return `"${last_pos === 0 ? str : result + str.slice(last_pos)}"`;
}
36 changes: 18 additions & 18 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,26 +167,26 @@ const fixtures = {
{
name: 'lone low surrogate',
value: 'a\uDC00b',
js: '"a\\uDC00b"',
json: '["a\\uDC00b"]'
js: '"a\uDC00b"',
json: '["a\uDC00b"]'
},
{
name: 'lone high surrogate',
value: 'a\uD800b',
js: '"a\\uD800b"',
json: '["a\\uD800b"]'
js: '"a\uD800b"',
json: '["a\uD800b"]'
},
{
name: 'two low surrogates',
value: 'a\uDC00\uDC00b',
js: '"a\\uDC00\\uDC00b"',
json: '["a\\uDC00\\uDC00b"]'
js: '"a\uDC00\uDC00b"',
json: '["a\uDC00\uDC00b"]'
},
{
name: 'two high surrogates',
value: 'a\uD800\uD800b',
js: '"a\\uD800\\uD800b"',
json: '["a\\uD800\\uD800b"]'
js: '"a\uD800\uD800b"',
json: '["a\uD800\uD800b"]'
},
{
name: 'surrogate pair',
Expand All @@ -197,8 +197,8 @@ const fixtures = {
{
name: 'surrogate pair in wrong order',
value: 'a\uDC00\uD800b',
js: '"a\\uDC00\\uD800b"',
json: '["a\\uDC00\\uD800b"]'
js: '"a\uDC00\uD800b"',
json: '["a\uDC00\uD800b"]'
},
{
name: 'nul',
Expand All @@ -215,8 +215,8 @@ const fixtures = {
{
name: 'control character extremum',
value: '\u001F',
js: '"\\u001F"',
json: '["\\u001F"]'
js: '"\\u001f"',
json: '["\\u001f"]'
},
{
name: 'backslash',
Expand Down Expand Up @@ -342,20 +342,20 @@ const fixtures = {
{
name: 'Dangerous string',
value: `</script><script src='https://evil.com/script.js'>alert('pwned')</script><script>`,
js: `"\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fscript.js'\\u003Ealert('pwned')\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003E"`,
json: `["\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fscript.js'\\u003Ealert('pwned')\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003E"]`
js: `"\\u003C/script>\\u003Cscript src='https://evil.com/script.js'>alert('pwned')\\u003C/script>\\u003Cscript>"`,
json: `["\\u003C/script>\\u003Cscript src='https://evil.com/script.js'>alert('pwned')\\u003C/script>\\u003Cscript>"]`
},
{
name: 'Dangerous key',
value: { '<svg onload=alert("xss_works")>': 'bar' },
js: '{"\\u003Csvg onload=alert(\\"xss_works\\")\\u003E":"bar"}',
json: '[{"\\u003Csvg onload=alert(\\"xss_works\\")\\u003E":1},"bar"]'
js: '{"\\u003Csvg onload=alert(\\"xss_works\\")>":"bar"}',
json: '[{"\\u003Csvg onload=alert(\\"xss_works\\")>":1},"bar"]'
},
{
name: 'Dangerous regex',
value: /[</script><script>alert('xss')//]/,
js: `new RegExp("[\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003Ealert('xss')\\u002F\\u002F]", "")`,
json: `[["RegExp","[\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003Ealert('xss')\\u002F\\u002F]"]]`
js: `new RegExp("[\\u003C/script>\\u003Cscript>alert('xss')//]", "")`,
json: `[["RegExp","[\\u003C/script>\\u003Cscript>alert('xss')//]"]]`
}
],

Expand Down

0 comments on commit c8239e4

Please sign in to comment.