Skip to content

Commit 5530e84

Browse files
committed
speed up stringify string
1 parent 99e66d0 commit 5530e84

File tree

4 files changed

+63
-49
lines changed

4 files changed

+63
-49
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "devalue",
2+
"name": "devalue2",
33
"description": "Gets the job done when JSON.stringify can't",
44
"version": "4.3.0",
55
"repository": "Rich-Harris/devalue",

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,7 @@ 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(Object.prototype)
3532
.sort()
3633
.join('\0');
3734

@@ -51,35 +48,52 @@ export function get_type(thing) {
5148
return Object.prototype.toString.call(thing).slice(8, -1);
5249
}
5350

51+
/** @param {string} char */
52+
function get_escaped_char(char) {
53+
switch (char) {
54+
case '"':
55+
return '\\"';
56+
case '<':
57+
return '\\u003C';
58+
case '\\':
59+
return '\\\\';
60+
case '\n':
61+
return '\\n';
62+
case '\r':
63+
return '\\r';
64+
case '\t':
65+
return '\\t';
66+
case '\b':
67+
return '\\b';
68+
case '\f':
69+
return '\\f';
70+
case '\u2028':
71+
return '\\u2028';
72+
case '\u2029':
73+
return '\\u2029';
74+
default:
75+
return char < ' '
76+
? `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`
77+
: '';
78+
}
79+
}
80+
5481
/** @param {string} str */
5582
export function stringify_string(str) {
56-
let result = '"';
83+
let result = '';
84+
let last_pos = 0;
85+
const len = str.length;
5786

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);
70-
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;
87+
for (let i = 0; i < len; i += 1) {
88+
const char = str[i];
89+
const replacement = get_escaped_char(char);
90+
if (replacement) {
91+
result += str.slice(last_pos, i) + replacement;
92+
last_pos = i + 1;
8093
}
8194
}
8295

83-
result += '"';
84-
return result;
96+
result += str.slice(last_pos);
97+
98+
return `"${result}"`;
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)