Skip to content

Commit c8239e4

Browse files
authored
Merge pull request #65 from gtm-nayan/perf-stringify
perf: speed up stringify_string
2 parents 99e66d0 + ffc41d5 commit c8239e4

File tree

3 files changed

+62
-48
lines changed

3 files changed

+62
-48
lines changed

src/uneval.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from './utils.js';
99

1010
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$';
11-
const unsafe_chars = /[<>\b\f\n\r\t\0\u2028\u2029]/g;
11+
const unsafe_chars = /[<\b\f\n\r\t\0\u2028\u2029]/g;
1212
const reserved =
1313
/^(?: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)$/;
1414

src/utils.js

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
/** @type {Record<string, string>} */
22
export const escaped = {
33
'<': '\\u003C',
4-
'>': '\\u003E',
5-
'/': '\\u002F',
64
'\\': '\\\\',
75
'\b': '\\b',
86
'\f': '\\f',
97
'\n': '\\n',
108
'\r': '\\r',
119
'\t': '\\t',
12-
'\0': '\\u0000',
1310
'\u2028': '\\u2028',
1411
'\u2029': '\\u2029'
1512
};
@@ -31,7 +28,9 @@ export function is_primitive(thing) {
3128
return Object(thing) !== thing;
3229
}
3330

34-
const object_proto_names = Object.getOwnPropertyNames(Object.prototype)
31+
const object_proto_names = /* @__PURE__ */ Object.getOwnPropertyNames(
32+
Object.prototype
33+
)
3534
.sort()
3635
.join('\0');
3736

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

53+
/** @param {string} char */
54+
function get_escaped_char(char) {
55+
switch (char) {
56+
case '"':
57+
return '\\"';
58+
case '<':
59+
return '\\u003C';
60+
case '\\':
61+
return '\\\\';
62+
case '\n':
63+
return '\\n';
64+
case '\r':
65+
return '\\r';
66+
case '\t':
67+
return '\\t';
68+
case '\b':
69+
return '\\b';
70+
case '\f':
71+
return '\\f';
72+
case '\u2028':
73+
return '\\u2028';
74+
case '\u2029':
75+
return '\\u2029';
76+
default:
77+
return char < ' '
78+
? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`
79+
: '';
80+
}
81+
}
82+
5483
/** @param {string} str */
5584
export function stringify_string(str) {
56-
let result = '"';
57-
58-
for (let i = 0; i < str.length; i += 1) {
59-
const char = str.charAt(i);
60-
const code = char.charCodeAt(0);
61-
62-
if (char === '"') {
63-
result += '\\"';
64-
} else if (char in escaped) {
65-
result += escaped[char];
66-
} else if (code <= 0x001F) {
67-
result += `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`
68-
} else if (code >= 0xd800 && code <= 0xdfff) {
69-
const next = str.charCodeAt(i + 1);
85+
let result = '';
86+
let last_pos = 0;
87+
const len = str.length;
7088

71-
// If this is the beginning of a [high, low] surrogate pair,
72-
// add the next two characters, otherwise escape
73-
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
74-
result += char + str[++i];
75-
} else {
76-
result += `\\u${code.toString(16).toUpperCase()}`;
77-
}
78-
} else {
79-
result += char;
89+
for (let i = 0; i < len; i += 1) {
90+
const char = str[i];
91+
const replacement = get_escaped_char(char);
92+
if (replacement) {
93+
result += str.slice(last_pos, i) + replacement;
94+
last_pos = i + 1;
8095
}
8196
}
8297

83-
result += '"';
84-
return result;
98+
return `"${last_pos === 0 ? str : result + str.slice(last_pos)}"`;
8599
}

test/test.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -167,26 +167,26 @@ const fixtures = {
167167
{
168168
name: 'lone low surrogate',
169169
value: 'a\uDC00b',
170-
js: '"a\\uDC00b"',
171-
json: '["a\\uDC00b"]'
170+
js: '"a\uDC00b"',
171+
json: '["a\uDC00b"]'
172172
},
173173
{
174174
name: 'lone high surrogate',
175175
value: 'a\uD800b',
176-
js: '"a\\uD800b"',
177-
json: '["a\\uD800b"]'
176+
js: '"a\uD800b"',
177+
json: '["a\uD800b"]'
178178
},
179179
{
180180
name: 'two low surrogates',
181181
value: 'a\uDC00\uDC00b',
182-
js: '"a\\uDC00\\uDC00b"',
183-
json: '["a\\uDC00\\uDC00b"]'
182+
js: '"a\uDC00\uDC00b"',
183+
json: '["a\uDC00\uDC00b"]'
184184
},
185185
{
186186
name: 'two high surrogates',
187187
value: 'a\uD800\uD800b',
188-
js: '"a\\uD800\\uD800b"',
189-
json: '["a\\uD800\\uD800b"]'
188+
js: '"a\uD800\uD800b"',
189+
json: '["a\uD800\uD800b"]'
190190
},
191191
{
192192
name: 'surrogate pair',
@@ -197,8 +197,8 @@ const fixtures = {
197197
{
198198
name: 'surrogate pair in wrong order',
199199
value: 'a\uDC00\uD800b',
200-
js: '"a\\uDC00\\uD800b"',
201-
json: '["a\\uDC00\\uD800b"]'
200+
js: '"a\uDC00\uD800b"',
201+
json: '["a\uDC00\uD800b"]'
202202
},
203203
{
204204
name: 'nul',
@@ -215,8 +215,8 @@ const fixtures = {
215215
{
216216
name: 'control character extremum',
217217
value: '\u001F',
218-
js: '"\\u001F"',
219-
json: '["\\u001F"]'
218+
js: '"\\u001f"',
219+
json: '["\\u001f"]'
220220
},
221221
{
222222
name: 'backslash',
@@ -342,20 +342,20 @@ const fixtures = {
342342
{
343343
name: 'Dangerous string',
344344
value: `</script><script src='https://evil.com/script.js'>alert('pwned')</script><script>`,
345-
js: `"\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fscript.js'\\u003Ealert('pwned')\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003E"`,
346-
json: `["\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fscript.js'\\u003Ealert('pwned')\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003E"]`
345+
js: `"\\u003C/script>\\u003Cscript src='https://evil.com/script.js'>alert('pwned')\\u003C/script>\\u003Cscript>"`,
346+
json: `["\\u003C/script>\\u003Cscript src='https://evil.com/script.js'>alert('pwned')\\u003C/script>\\u003Cscript>"]`
347347
},
348348
{
349349
name: 'Dangerous key',
350350
value: { '<svg onload=alert("xss_works")>': 'bar' },
351-
js: '{"\\u003Csvg onload=alert(\\"xss_works\\")\\u003E":"bar"}',
352-
json: '[{"\\u003Csvg onload=alert(\\"xss_works\\")\\u003E":1},"bar"]'
351+
js: '{"\\u003Csvg onload=alert(\\"xss_works\\")>":"bar"}',
352+
json: '[{"\\u003Csvg onload=alert(\\"xss_works\\")>":1},"bar"]'
353353
},
354354
{
355355
name: 'Dangerous regex',
356356
value: /[</script><script>alert('xss')//]/,
357-
js: `new RegExp("[\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003Ealert('xss')\\u002F\\u002F]", "")`,
358-
json: `[["RegExp","[\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003Ealert('xss')\\u002F\\u002F]"]]`
357+
js: `new RegExp("[\\u003C/script>\\u003Cscript>alert('xss')//]", "")`,
358+
json: `[["RegExp","[\\u003C/script>\\u003Cscript>alert('xss')//]"]]`
359359
}
360360
],
361361

0 commit comments

Comments
 (0)