Skip to content

Commit d161dfe

Browse files
committed
Merge branch 'main' of https://github.com/lxsmnsyc/seroval
2 parents ab289cf + 067d4bf commit d161dfe

File tree

10 files changed

+162
-112
lines changed

10 files changed

+162
-112
lines changed

packages/seroval/src/core/context/serializer.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -446,12 +446,24 @@ export default abstract class BaseSerializerContext
446446
): string {
447447
if (typeof key === 'string') {
448448
const check = Number(key);
449-
// Test if key is a valid number or JS identifier
450-
// so that we don't have to serialize the key and wrap with brackets
451-
const isIdentifier = check >= 0 || isValidIdentifier(key);
449+
const isIdentifier = (
450+
// Test if key is a valid positive number or JS identifier
451+
// so that we don't have to serialize the key and wrap with brackets
452+
check >= 0
453+
// It's also important to consider that if the key is
454+
// indeed numeric, we need to make sure that when
455+
// converted back into a string, it's still the same
456+
// to the original key. This allows us to differentiate
457+
// keys that has numeric formats but in a different
458+
// format, which can cause unintentional key declaration
459+
// Example: { 0x1: 1 } vs { '0x1': 1 }
460+
&& check.toString() === key
461+
) || isValidIdentifier(key);
452462
if (this.isIndexedValueInStack(val)) {
453463
const refParam = this.getRefParam((val as SerovalIndexedValueNode).i);
454464
this.markRef(source.i);
465+
// Strict identifier check, make sure
466+
// that it isn't numeric (except NaN)
455467
if (isIdentifier && check !== check) {
456468
this.createObjectAssign(source.i, key, refParam);
457469
} else {
@@ -516,10 +528,22 @@ export default abstract class BaseSerializerContext
516528
): void {
517529
const serialized = this.serialize(value);
518530
const check = Number(key);
519-
// Test if key is a valid number or JS identifier
520-
// so that we don't have to serialize the key and wrap with brackets
521-
const isIdentifier = check >= 0 || isValidIdentifier(key);
531+
const isIdentifier = (
532+
// Test if key is a valid positive number or JS identifier
533+
// so that we don't have to serialize the key and wrap with brackets
534+
check >= 0
535+
// It's also important to consider that if the key is
536+
// indeed numeric, we need to make sure that when
537+
// converted back into a string, it's still the same
538+
// to the original key. This allows us to differentiate
539+
// keys that has numeric formats but in a different
540+
// format, which can cause unintentional key declaration
541+
// Example: { 0x1: 1 } vs { '0x1': 1 }
542+
&& check.toString() === key
543+
) || isValidIdentifier(key);
522544
if (this.isIndexedValueInStack(value)) {
545+
// Strict identifier check, make sure
546+
// that it isn't numeric (except NaN)
523547
if (isIdentifier && check !== check) {
524548
this.createObjectAssign(source.i, key, serialized);
525549
} else {
@@ -532,7 +556,7 @@ export default abstract class BaseSerializerContext
532556
} else {
533557
const parentAssignment = this.assignments;
534558
this.assignments = mainAssignments;
535-
if (isIdentifier) {
559+
if (isIdentifier && check !== check) {
536560
this.createObjectAssign(source.i, key, serialized);
537561
} else {
538562
this.createArrayAssign(

packages/seroval/test/__snapshots__/frozen-object.test.ts.snap

Lines changed: 13 additions & 13 deletions
Large diffs are not rendered by default.

packages/seroval/test/__snapshots__/null-constructor.test.ts.snap

Lines changed: 16 additions & 16 deletions
Large diffs are not rendered by default.

packages/seroval/test/__snapshots__/object.test.ts.snap

Lines changed: 16 additions & 16 deletions
Large diffs are not rendered by default.

packages/seroval/test/__snapshots__/sealed-object.test.ts.snap

Lines changed: 13 additions & 13 deletions
Large diffs are not rendered by default.

packages/seroval/test/frozen-object.test.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ import {
1515
toJSONAsync,
1616
} from '../src';
1717

18-
const EXAMPLE = Object.freeze({ hello: 'world' });
18+
const EXAMPLE = Object.freeze({
19+
example: 'valid identifier',
20+
'%example': 'invalid identifier',
21+
'0x1': 'hexadecimal',
22+
'0b1': 'binary',
23+
'0o1': 'octal',
24+
'1_000': 'numeric separator',
25+
'1.7976931348623157e+308': 'exponentiation',
26+
});
1927

2028
const RECURSIVE = {} as Record<string, unknown>;
2129
RECURSIVE.a = RECURSIVE;
@@ -52,7 +60,7 @@ describe('frozen object', () => {
5260
const back = deserialize<typeof EXAMPLE>(result);
5361
expect(Object.isFrozen(back)).toBe(true);
5462
expect(back.constructor).toBe(Object);
55-
expect(back.hello).toBe(EXAMPLE.hello);
63+
expect(back.example).toBe(EXAMPLE.example);
5664
});
5765
it('supports self-recursion', () => {
5866
const result = serialize(RECURSIVE);
@@ -78,7 +86,7 @@ describe('frozen object', () => {
7886
const back = await deserialize<Promise<typeof EXAMPLE>>(result);
7987
expect(Object.isFrozen(back)).toBe(true);
8088
expect(back.constructor).toBe(Object);
81-
expect(back.hello).toBe(EXAMPLE.hello);
89+
expect(back.example).toBe(EXAMPLE.example);
8290
});
8391
it('supports self-recursion', async () => {
8492
const result = await serializeAsync(ASYNC_RECURSIVE);
@@ -112,7 +120,7 @@ describe('frozen object', () => {
112120
const back = fromJSON<typeof EXAMPLE>(result);
113121
expect(Object.isFrozen(back)).toBe(true);
114122
expect(back.constructor).toBe(Object);
115-
expect(back.hello).toBe(EXAMPLE.hello);
123+
expect(back.example).toBe(EXAMPLE.example);
116124
});
117125
it('supports self-recursion', () => {
118126
const result = toJSON(RECURSIVE);
@@ -138,7 +146,7 @@ describe('frozen object', () => {
138146
const back = await fromJSON<Promise<typeof EXAMPLE>>(result);
139147
expect(Object.isFrozen(back)).toBe(true);
140148
expect(back.constructor).toBe(Object);
141-
expect(back.hello).toBe(EXAMPLE.hello);
149+
expect(back.example).toBe(EXAMPLE.example);
142150
});
143151
it('supports self-recursion', async () => {
144152
const result = await toJSONAsync(ASYNC_RECURSIVE);
@@ -167,9 +175,7 @@ describe('frozen object', () => {
167175
});
168176
describe('crossSerialize', () => {
169177
it('supports Objects', () => {
170-
const example = { hello: 'world' } as { hello: string };
171-
Object.freeze(example);
172-
const result = crossSerialize(example);
178+
const result = crossSerialize(EXAMPLE);
173179
expect(result).toMatchSnapshot();
174180
});
175181
it('supports self-recursion', () => {
@@ -368,7 +374,7 @@ describe('frozen object', () => {
368374
});
369375
expect(Object.isFrozen(back)).toBe(true);
370376
expect(back.constructor).toBe(Object);
371-
expect(back.hello).toBe(EXAMPLE.hello);
377+
expect(back.example).toBe(EXAMPLE.example);
372378
});
373379
it('supports self-recursion', () => {
374380
const result = toCrossJSON(RECURSIVE);
@@ -400,7 +406,7 @@ describe('frozen object', () => {
400406
});
401407
expect(Object.isFrozen(back)).toBe(true);
402408
expect(back.constructor).toBe(Object);
403-
expect(back.hello).toBe(EXAMPLE.hello);
409+
expect(back.example).toBe(EXAMPLE.example);
404410
});
405411
it('supports self-recursion', async () => {
406412
const result = await toCrossJSONAsync(ASYNC_RECURSIVE);

packages/seroval/test/null-constructor.test.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@ import {
1717
toJSONAsync,
1818
} from '../src';
1919

20-
const EXAMPLE = Object.assign(Object.create(null), { hello: 'world' }) as {
21-
hello: string;
22-
};
20+
const EXAMPLE = Object.assign(Object.create(null), {
21+
example: 'valid identifier',
22+
'%example': 'invalid identifier',
23+
'0x1': 'hexadecimal',
24+
'0b1': 'binary',
25+
'0o1': 'octal',
26+
'1_000': 'numeric separator',
27+
'1.7976931348623157e+308': 'exponentiation',
28+
});
2329

2430
const RECURSIVE = Object.create(null) as Record<string, unknown>;
2531
RECURSIVE.a = RECURSIVE;
@@ -53,7 +59,7 @@ describe('null-constructor', () => {
5359
expect(result).toMatchSnapshot();
5460
const back = deserialize<typeof EXAMPLE>(result);
5561
expect(back.constructor).toBeUndefined();
56-
expect(back.hello).toBe(EXAMPLE.hello);
62+
expect(back.example).toBe(EXAMPLE.example);
5763
});
5864
it('supports self-recursion', () => {
5965
const result = serialize(RECURSIVE);
@@ -76,7 +82,7 @@ describe('null-constructor', () => {
7682
expect(result).toMatchSnapshot();
7783
const back = await deserialize<Promise<typeof EXAMPLE>>(result);
7884
expect(back.constructor).toBeUndefined();
79-
expect(back.hello).toBe(EXAMPLE.hello);
85+
expect(back.example).toBe(EXAMPLE.example);
8086
});
8187
it('supports self-recursion', async () => {
8288
const result = await serializeAsync(ASYNC_RECURSIVE);
@@ -106,7 +112,7 @@ describe('null-constructor', () => {
106112
expect(JSON.stringify(result)).toMatchSnapshot();
107113
const back = fromJSON<typeof EXAMPLE>(result);
108114
expect(back.constructor).toBeUndefined();
109-
expect(back.hello).toBe(EXAMPLE.hello);
115+
expect(back.example).toBe(EXAMPLE.example);
110116
});
111117
it('supports self-recursion', () => {
112118
const result = toJSON(RECURSIVE);
@@ -129,7 +135,7 @@ describe('null-constructor', () => {
129135
expect(JSON.stringify(result)).toMatchSnapshot();
130136
const back = await fromJSON<Promise<typeof EXAMPLE>>(result);
131137
expect(back.constructor).toBeUndefined();
132-
expect(back.hello).toBe(EXAMPLE.hello);
138+
expect(back.example).toBe(EXAMPLE.example);
133139
});
134140
it('supports self-recursion', async () => {
135141
const result = await toJSONAsync(ASYNC_RECURSIVE);
@@ -155,9 +161,7 @@ describe('null-constructor', () => {
155161
});
156162
describe('crossSerialize', () => {
157163
it('supports Object.create(null)', () => {
158-
const example = { hello: 'world' } as { hello: string };
159-
Object.freeze(example);
160-
const result = crossSerialize(example);
164+
const result = crossSerialize(EXAMPLE);
161165
expect(result).toMatchSnapshot();
162166
});
163167
it('supports self-recursion', () => {
@@ -356,7 +360,7 @@ describe('null-constructor', () => {
356360
});
357361

358362
expect(back.constructor).toBeUndefined();
359-
expect(back.hello).toBe(EXAMPLE.hello);
363+
expect(back.example).toBe(EXAMPLE.example);
360364
});
361365
it('supports self-recursion', () => {
362366
const result = toCrossJSON(RECURSIVE);
@@ -388,7 +392,7 @@ describe('null-constructor', () => {
388392
});
389393

390394
expect(back.constructor).toBeUndefined();
391-
expect(back.hello).toBe(EXAMPLE.hello);
395+
expect(back.example).toBe(EXAMPLE.example);
392396
});
393397
it('supports self-recursion', async () => {
394398
const result = await toCrossJSONAsync(ASYNC_RECURSIVE);
@@ -485,7 +489,7 @@ describe('null-constructor', () => {
485489
expect(result).toMatchSnapshot();
486490
const back = deserialize<typeof EXAMPLE>(result);
487491
expect(back.constructor).toBeUndefined();
488-
expect(back.hello).toBe(EXAMPLE.hello);
492+
expect(back.example).toBe(EXAMPLE.example);
489493
});
490494
});
491495
describe('compat#toJSON', () => {
@@ -497,7 +501,7 @@ describe('null-constructor', () => {
497501
expect(compileJSON(result)).toMatchSnapshot();
498502
const back = fromJSON<typeof EXAMPLE>(result);
499503
expect(back.constructor).toBeUndefined();
500-
expect(back.hello).toBe(EXAMPLE.hello);
504+
expect(back.example).toBe(EXAMPLE.example);
501505
});
502506
});
503507
});

packages/seroval/test/object.test.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@ import {
1717
toJSONAsync,
1818
} from '../src';
1919

20-
const EXAMPLE = { hello: 'world' };
20+
const EXAMPLE = {
21+
example: 'valid identifier',
22+
'%example': 'invalid identifier',
23+
'0x1': 'hexadecimal',
24+
'0b1': 'binary',
25+
'0o1': 'octal',
26+
'1_000': 'numeric separator',
27+
'1.7976931348623157e+308': 'exponentiation',
28+
};
2129

2230
const RECURSIVE = {} as Record<string, unknown>;
2331
RECURSIVE.a = RECURSIVE;
@@ -51,7 +59,7 @@ describe('objects', () => {
5159
expect(result).toMatchSnapshot();
5260
const back = deserialize<typeof EXAMPLE>(result);
5361
expect(back.constructor).toBe(Object);
54-
expect(back.hello).toBe(EXAMPLE.hello);
62+
expect(back.example).toBe(EXAMPLE.example);
5563
});
5664
it('supports self-recursion', () => {
5765
const result = serialize(RECURSIVE);
@@ -74,7 +82,7 @@ describe('objects', () => {
7482
expect(result).toMatchSnapshot();
7583
const back = await deserialize<Promise<typeof EXAMPLE>>(result);
7684
expect(back.constructor).toBe(Object);
77-
expect(back.hello).toBe(EXAMPLE.hello);
85+
expect(back.example).toBe(EXAMPLE.example);
7886
});
7987
it('supports self-recursion', async () => {
8088
const result = await serializeAsync(ASYNC_RECURSIVE);
@@ -104,7 +112,7 @@ describe('objects', () => {
104112
expect(JSON.stringify(result)).toMatchSnapshot();
105113
const back = fromJSON<typeof EXAMPLE>(result);
106114
expect(back.constructor).toBe(Object);
107-
expect(back.hello).toBe(EXAMPLE.hello);
115+
expect(back.example).toBe(EXAMPLE.example);
108116
});
109117
it('supports self-recursion', () => {
110118
const result = toJSON(RECURSIVE);
@@ -127,7 +135,7 @@ describe('objects', () => {
127135
expect(JSON.stringify(result)).toMatchSnapshot();
128136
const back = await fromJSON<Promise<typeof EXAMPLE>>(result);
129137
expect(back.constructor).toBe(Object);
130-
expect(back.hello).toBe(EXAMPLE.hello);
138+
expect(back.example).toBe(EXAMPLE.example);
131139
});
132140
it('supports self-recursion', async () => {
133141
const result = await toJSONAsync(ASYNC_RECURSIVE);
@@ -153,9 +161,7 @@ describe('objects', () => {
153161
});
154162
describe('crossSerialize', () => {
155163
it('supports Objects', () => {
156-
const example = { hello: 'world' } as { hello: string };
157-
Object.freeze(example);
158-
const result = crossSerialize(example);
164+
const result = crossSerialize(EXAMPLE);
159165
expect(result).toMatchSnapshot();
160166
});
161167
it('supports self-recursion', () => {
@@ -354,7 +360,7 @@ describe('objects', () => {
354360
});
355361

356362
expect(back.constructor).toBe(Object);
357-
expect(back.hello).toBe(EXAMPLE.hello);
363+
expect(back.example).toBe(EXAMPLE.example);
358364
});
359365
it('supports self-recursion', () => {
360366
const result = toCrossJSON(RECURSIVE);
@@ -386,7 +392,7 @@ describe('objects', () => {
386392
});
387393

388394
expect(back.constructor).toBe(Object);
389-
expect(back.hello).toBe(EXAMPLE.hello);
395+
expect(back.example).toBe(EXAMPLE.example);
390396
});
391397
it('supports self-recursion', async () => {
392398
const result = await toCrossJSONAsync(ASYNC_RECURSIVE);
@@ -483,7 +489,7 @@ describe('objects', () => {
483489
expect(result).toMatchSnapshot();
484490
const back = deserialize<typeof EXAMPLE>(result);
485491
expect(back.constructor).toBe(Object);
486-
expect(back.hello).toBe(EXAMPLE.hello);
492+
expect(back.example).toBe(EXAMPLE.example);
487493
});
488494
});
489495
describe('compat#toJSON', () => {
@@ -495,7 +501,7 @@ describe('objects', () => {
495501
expect(compileJSON(result)).toMatchSnapshot();
496502
const back = fromJSON<typeof EXAMPLE>(result);
497503
expect(back.constructor).toBe(Object);
498-
expect(back.hello).toBe(EXAMPLE.hello);
504+
expect(back.example).toBe(EXAMPLE.example);
499505
});
500506
});
501507
});

0 commit comments

Comments
 (0)