fast-json-stringify 7.1.0 → 7.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,31 @@ test('error on invalid largeArrayMechanism', (t) => {
23
23
  }), Error('Unsupported large array mechanism invalid'))
24
24
  })
25
25
 
26
+ test('does not leak largeArrayMechanism across build() calls', (t) => {
27
+ t.plan(2)
28
+
29
+ const users = []
30
+ for (let i = 0; i < 100; i++) users.push({ name: 'user' + i, password: 'secret' + i })
31
+
32
+ const schema = {
33
+ type: 'array',
34
+ items: {
35
+ type: 'object',
36
+ properties: {
37
+ name: { type: 'string' }
38
+ }
39
+ }
40
+ }
41
+
42
+ // An unrelated build() opts into json-stringify for its own schema.
43
+ const optedIn = build({ type: 'array' }, { largeArrayMechanism: 'json-stringify', largeArraySize: 100 })
44
+ t.assert.equal(optedIn(users).includes('password'), true)
45
+
46
+ // A later build() without options must not inherit the mechanism.
47
+ const stringify = build(schema)
48
+ t.assert.equal(stringify(users).includes('password'), false)
49
+ })
50
+
26
51
  function buildTest (schema, toStringify, options) {
27
52
  test(`render a ${schema.title} as JSON`, (t) => {
28
53
  t.plan(3)
@@ -686,3 +711,125 @@ buildTest({
686
711
  largeArraySize: '10000',
687
712
  largeArrayMechanism: 'default'
688
713
  })
714
+
715
+ buildTest({
716
+ title: 'string array whose items declare const',
717
+ type: 'array',
718
+ items: { type: 'string', const: 'a' }
719
+ }, ['a', 'a'])
720
+
721
+ buildTest({
722
+ title: 'string array whose items declare allOf',
723
+ type: 'array',
724
+ items: { type: 'string', allOf: [{ type: 'string' }] }
725
+ }, ['a', 'b'])
726
+
727
+ buildTest({
728
+ title: 'string array whose items declare anyOf',
729
+ type: 'array',
730
+ items: { type: 'string', anyOf: [{ type: 'string' }] }
731
+ }, ['a', 'b'])
732
+
733
+ buildTest({
734
+ title: 'string array whose items declare oneOf',
735
+ type: 'array',
736
+ items: { type: 'string', oneOf: [{ type: 'string' }] }
737
+ }, ['a', 'b'])
738
+
739
+ buildTest({
740
+ title: 'string array whose items declare if',
741
+ type: 'array',
742
+ items: { type: 'string', if: { type: 'string' }, then: { type: 'string' } }
743
+ }, ['a', 'b'])
744
+
745
+ test('string array coerces a non-string item', (t) => {
746
+ const stringify = build({
747
+ type: 'array',
748
+ items: { type: 'string' }
749
+ }, { largeArraySize: 2 })
750
+ const date = new Date('2020-01-02T03:04:05.006Z')
751
+
752
+ t.assert.equal(
753
+ stringify([1, 'a', null, date, /re/]),
754
+ '["1","a","","2020-01-02T03:04:05.006Z","re"]'
755
+ )
756
+ })
757
+
758
+ // Large number/integer arrays are copied and handed to JSON.stringify. The
759
+ // first element that is not already the right type switches to the per-item
760
+ // loop from that index on. 3e5 elements is the largest fast-path cutoff on
761
+ // any supported Node release (see PRIMITIVE_FAST_PATH_MIN_LENGTH in index.js),
762
+ // so these always exercise the resume path.
763
+ const FAST_PATH_LENGTH = 3e5
764
+
765
+ for (const [type, bad, expected] of [
766
+ ['integer', 1.9, '1'],
767
+ ['integer', '7', '7'],
768
+ ['integer', null, '0'],
769
+ ['number', '7.5', '7.5'],
770
+ ['number', null, '0'],
771
+ ['number', true, '1']
772
+ ]) {
773
+ for (const [position, index] of [
774
+ ['start', 0],
775
+ ['middle', FAST_PATH_LENGTH >> 1],
776
+ ['end', FAST_PATH_LENGTH - 1]
777
+ ]) {
778
+ test(`large ${type} array falls back to the loop on ${JSON.stringify(bad)} at the ${position}`, (t) => {
779
+ const stringify = build({ type: 'array', items: { type } })
780
+ const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i - 1000)
781
+ input[index] = bad
782
+
783
+ const reference = Array.from(input, (v, i) => i === index ? expected : String(v))
784
+ t.assert.equal(stringify(input), '[' + reference.join(',') + ']')
785
+ })
786
+ }
787
+ }
788
+
789
+ test('large integer array applies the rounding option after the fallback', (t) => {
790
+ const stringify = build({ type: 'array', items: { type: 'integer' } }, { rounding: 'ceil' })
791
+ const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i)
792
+ input[10] = 1.1
793
+ input[FAST_PATH_LENGTH - 1] = 2.1
794
+
795
+ const output = stringify(input)
796
+ t.assert.equal(output.slice(0, 23), '[0,1,2,3,4,5,6,7,8,9,2,')
797
+ t.assert.equal(output.slice(-3), ',3]')
798
+ })
799
+
800
+ test('large number array still throws on a non-numeric string after the fallback', (t) => {
801
+ const stringify = build({ type: 'array', items: { type: 'number' } })
802
+ const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i / 10)
803
+ input[FAST_PATH_LENGTH >> 1] = 'not a number'
804
+
805
+ t.assert.throws(() => stringify(input), new Error('The value "not a number" cannot be converted to a number.'))
806
+ })
807
+
808
+ // A real NaN is typeof 'number', so only the `v === v` half of the fast-path
809
+ // condition rejects it. Check it at every position the copy loop can stop at.
810
+ for (const [position, index] of [['start', 0], ['middle', FAST_PATH_LENGTH >> 1], ['end', FAST_PATH_LENGTH - 1]]) {
811
+ test(`large number array still throws on NaN at the ${position} after the fallback`, (t) => {
812
+ const stringify = build({ type: 'array', items: { type: 'number' } })
813
+ const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i / 10)
814
+ input[index] = NaN
815
+
816
+ t.assert.throws(() => stringify(input), new Error('The value "NaN" cannot be converted to a number.'))
817
+ })
818
+ }
819
+
820
+ test('large number array reads each element once across the fallback', (t) => {
821
+ const stringify = build({ type: 'array', items: { type: 'number' } })
822
+ const input = Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i)
823
+ const reads = []
824
+ for (const index of [0, 1, FAST_PATH_LENGTH >> 1, FAST_PATH_LENGTH - 1]) {
825
+ Object.defineProperty(input, index, {
826
+ get () {
827
+ reads.push(index)
828
+ return index === 1 ? '1' : index
829
+ }
830
+ })
831
+ }
832
+
833
+ t.assert.equal(stringify(input), JSON.stringify(Array.from({ length: FAST_PATH_LENGTH }, (_, i) => i)))
834
+ t.assert.deepEqual(reads, [0, 1, FAST_PATH_LENGTH >> 1, FAST_PATH_LENGTH - 1])
835
+ })
@@ -110,11 +110,23 @@ test('rejects invalid maximum schema depths', (t) => {
110
110
  test('checks schema dependencies without treating property lists as schemas', (t) => {
111
111
  t.assert.doesNotThrow(() => build({
112
112
  type: 'object',
113
+ definitions: {
114
+ valueProp: { type: 'string' },
115
+ any: true
116
+ },
117
+ // Include a $ref so recursive-schema detection runs and walks dependency
118
+ // schema forms (property-list dependencies must still be ignored). Boolean
119
+ // schemas must also be visited so traverse's non-object early-return stays covered.
120
+ properties: {
121
+ link: { $ref: '#/definitions/valueProp' },
122
+ flag: { $ref: '#/definitions/any' },
123
+ open: true
124
+ },
113
125
  dependencies: {
114
- enabled: { properties: { value: { type: 'string' } } },
126
+ enabled: { properties: { value: { $ref: '#/definitions/valueProp' } } },
115
127
  value: ['enabled']
116
128
  }
117
- }, { maxDepth: 2 }))
129
+ }, { maxDepth: 3 }))
118
130
  })
119
131
 
120
132
  test('checks external schemas before validating them recursively', (t) => {