Skip to content

Commit 4c231ac

Browse files
committed
get stringify to work
1 parent 0f5789e commit 4c231ac

File tree

4 files changed

+68
-41
lines changed

4 files changed

+68
-41
lines changed

src/devalue.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
escaped,
44
get_type,
55
is_primitive,
6-
stringify_primitive,
76
stringify_string
87
} from './utils.js';
98

@@ -288,3 +287,14 @@ function safe_prop(key) {
288287
? `.${key}`
289288
: `[${escape_unsafe_chars(JSON.stringify(key))}]`;
290289
}
290+
291+
/** @param {any} thing */
292+
function stringify_primitive(thing) {
293+
if (typeof thing === 'string') return stringify_string(thing);
294+
if (thing === void 0) return 'void 0';
295+
if (thing === 0 && 1 / thing < 0) return '-0';
296+
const str = String(thing);
297+
if (typeof thing === 'number') return str.replace(/^(-)?0\./, '$1.');
298+
if (typeof thing === 'bigint') return thing + 'n';
299+
return str;
300+
}

src/stringify.js

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
get_type,
44
is_plain_object,
55
is_primitive,
6-
stringify_primitive
6+
stringify_string
77
} from './utils.js';
88

99
const UNDEFINED = -1;
@@ -19,14 +19,16 @@ const NEGATIVE_ZERO = -6;
1919
*/
2020
export function stringify(value) {
2121
/** @type {any[]} */
22-
const array = [];
22+
const stringified = [];
2323

2424
/** @type {Map<any, number>} */
2525
const map = new Map();
2626

2727
/** @type {string[]} */
2828
const keys = [];
2929

30+
let p = 0;
31+
3032
/** @param {any} thing */
3133
function flatten(thing) {
3234
if (map.has(thing)) return map.get(thing);
@@ -37,42 +39,43 @@ export function stringify(value) {
3739
if (thing === -Infinity) return NEGATIVE_INFINITY;
3840
if (thing === 0 && 1 / thing < 0) return NEGATIVE_ZERO;
3941

40-
const index = array.length;
42+
const index = p++;
4143
map.set(thing, index);
4244

4345
if (typeof thing === 'function') {
4446
throw new DevalueError(`Cannot stringify a function`, keys);
4547
}
4648

4749
if (is_primitive(thing)) {
48-
array.push(thing);
50+
stringified[index] = stringify_primitive(thing);
4951
} else {
5052
const type = get_type(thing);
5153

5254
switch (type) {
5355
case 'Number':
5456
case 'String':
5557
case 'Boolean':
56-
array.push(['Object', thing]);
58+
stringified[index] = `["Object",${stringify_primitive(thing)}]`;
5759
break;
5860

5961
case 'BigInt':
60-
array.push(['BigInt', thing.toString()]);
62+
stringified[index] = `["BigInt",${thing}]`;
6163
break;
6264

6365
case 'Date':
64-
array.push(['Date', thing.toISOString()]);
66+
stringified[index] = `["Date","${thing.toISOString()}"]`;
6567
break;
6668

6769
case 'RegExp':
6870
const { source, flags } = thing;
69-
array.push(flags ? ['RegExp', source, flags] : ['RegExp', source]);
71+
stringified[index] = flags
72+
? `["RegExp",${stringify_string(source)},"${flags}"]`
73+
: `["RegExp",${stringify_string(source)}]`;
7074
break;
7175

7276
case 'Array':
7377
/** @type {number[]} */
74-
const flattened_array = [];
75-
array.push(flattened_array);
78+
let flattened_array = [];
7679

7780
for (let i = 0; i < thing.length; i += 1) {
7881
if (i in thing) {
@@ -84,29 +87,33 @@ export function stringify(value) {
8487
}
8588
}
8689

90+
stringified[index] = `[${flattened_array.join(',')}]`;
91+
8792
break;
8893

8994
case 'Set':
90-
/** @type {any[]} */
91-
const flattened_set = ['Set', []];
92-
array.push(flattened_set);
95+
/** @type {number[]} */
96+
const flattened_set = [];
9397

9498
for (const value of thing) {
95-
flattened_set[1].push(flatten(value));
99+
flattened_set.push(flatten(value));
96100
}
101+
102+
stringified[index] = `["Set",[${flattened_set.join(',')}]]`;
97103
break;
98104

99105
case 'Map':
100-
/** @type {any[]} */
101-
const flattened_map = ['Map', []];
102-
array.push(flattened_map);
106+
/** @type {number[]} */
107+
const flattened_map = [];
103108

104109
for (const [key, value] of thing) {
105110
keys.push(
106111
`.get(${is_primitive(key) ? stringify_primitive(key) : '...'})`
107112
);
108-
flattened_map[1].push(flatten(key), flatten(value));
113+
flattened_map.push(flatten(key), flatten(value));
109114
}
115+
116+
stringified[index] = `["Map",[${flattened_map.join(',')}]]`;
110117
break;
111118

112119
default:
@@ -124,15 +131,22 @@ export function stringify(value) {
124131
);
125132
}
126133

127-
/** @type {Record<string, any>} */
128-
const flattened_object = {};
129-
array.push(flattened_object);
134+
/** @type {string[]} */
135+
const flattened_object = [];
130136

131137
for (const key in thing) {
132138
keys.push(`.${key}`);
133-
flattened_object[key] = flatten(thing[key]);
139+
flattened_object.push(
140+
`${stringify_string(key)}:${flatten(thing[key])}`
141+
);
134142
keys.pop();
135143
}
144+
145+
stringified[index] = `{${flattened_object.join(',')}}`;
146+
147+
if (Object.getPrototypeOf(thing) === null) {
148+
stringified[index] = `["null",${stringified[index]}]`;
149+
}
136150
}
137151
}
138152

@@ -144,5 +158,19 @@ export function stringify(value) {
144158
// special case — value is represented as a negative index
145159
if (index < 0) return `[${index}]`;
146160

147-
return JSON.stringify(array);
161+
return `[${stringified.join(',')}]`;
162+
}
163+
164+
/**
165+
* @param {any} thing
166+
* @returns {string}
167+
*/
168+
function stringify_primitive(thing) {
169+
const type = typeof thing;
170+
if (type === 'string') return stringify_string(thing);
171+
if (thing instanceof String) return stringify_string(thing.toString());
172+
if (thing === void 0) return UNDEFINED.toString();
173+
if (thing === 0 && 1 / thing < 0) return NEGATIVE_ZERO.toString();
174+
if (type === 'bigint') return `["BigInt","${thing}"]`;
175+
return String(thing);
148176
}

src/utils.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,6 @@ export function get_type(thing) {
5151
return Object.prototype.toString.call(thing).slice(8, -1);
5252
}
5353

54-
/** @param {any} thing */
55-
export function stringify_primitive(thing) {
56-
if (typeof thing === 'string') return stringify_string(thing);
57-
if (thing === void 0) return 'void 0';
58-
if (thing === 0 && 1 / thing < 0) return '-0';
59-
const str = String(thing);
60-
if (typeof thing === 'number') return str.replace(/^(-)?0\./, '$1.');
61-
if (typeof thing === 'bigint') return thing + 'n';
62-
return str;
63-
}
64-
6554
/** @param {string} str */
6655
export function stringify_string(str) {
6756
let result = '"';

test/test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,10 @@ const fixtures = {
253253
((obj) => {
254254
obj.self = obj;
255255
return {
256-
name: 'Object (cyclical)',
256+
name: 'Object with null prototype (cyclical)',
257257
value: obj,
258258
js: '(function(a){a.self=a;return a}(Object.create(null)))',
259-
json: 'TODO'
259+
json: '[["null",{"self":0}]]'
260260
};
261261
})(Object.create(null)),
262262

@@ -286,19 +286,19 @@ const fixtures = {
286286
name: 'Dangerous string',
287287
value: `</script><script src='https://evil.com/script.js'>alert('pwned')</script><script>`,
288288
js: `"\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fscript.js'\\u003Ealert('pwned')\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003E"`,
289-
json: 'TODO'
289+
json: `["\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fscript.js'\\u003Ealert('pwned')\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003E"]`
290290
},
291291
{
292292
name: 'Dangerous key',
293293
value: { '<svg onload=alert("xss_works")>': 'bar' },
294294
js: '{"\\u003Csvg onload=alert(\\"xss_works\\")\\u003E":"bar"}',
295-
json: 'TODO'
295+
json: '[{"\\u003Csvg onload=alert(\\"xss_works\\")\\u003E":1},"bar"]'
296296
},
297297
{
298298
name: 'Dangerous regex',
299299
value: /[</script><script>alert('xss')//]/,
300300
js: `new RegExp("[\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003Ealert('xss')\\u002F\\u002F]", "")`,
301-
json: `["RegExp","[\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003Ealert('xss')\\u002F\\u002F]",""]`
301+
json: `[["RegExp","[\\u003C\\u002Fscript\\u003E\\u003Cscript\\u003Ealert('xss')\\u002F\\u002F]"]]`
302302
}
303303
],
304304

@@ -307,7 +307,7 @@ const fixtures = {
307307
name: 'Object without prototype',
308308
value: Object.create(null),
309309
js: 'Object.create(null)',
310-
json: '[["Object"]]'
310+
json: '[["null",{}]]'
311311
},
312312
{
313313
name: 'cross-realm POJO',

0 commit comments

Comments
 (0)